Repository: Deltares/pandamesh (language: python)

Issue:
TriangleMesher: cell size to area conversion
Triangle requires an area for triangles, rather than a specified cell size. Gmsh uses a cell size.

To make things consistent across, I compute an area based on the cell size:

`0.5 * cellsize * cellsize`

I.e. $A = \frac{1}{2} b h$

This is assuming a right-angled triangled where base and height are equal. Of course, this means that the hypothenuse is longer than the specified cell size.

The ideal triangle is probably equilateral, in which case $A = \frac{\sqrt{3}}{4} s^2$.

This results in slightly smaller triangles.



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index bf423fd..aaa655a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,4 +25,7 @@ dist/*
 .coverage
 coverage.xml
 
-sg_execution_times.rst
\ No newline at end of file
+sg_execution_times.rst
+docs/api/api/*
+
+__pycache__
\ No newline at end of file
diff --git a/docs/_templates/enums.rst b/docs/_templates/enums.rst
new file mode 100644
index 0000000..f0cde40
--- /dev/null
+++ b/docs/_templates/enums.rst
@@ -0,0 +1,7 @@
+{{ fullname | escape | underline}}
+
+.. currentmodule:: {{ module }}
+
+.. autoclass:: {{ objname }}
+   :members:
+   :member-order: bysource
diff --git a/docs/api/changelog.rst b/docs/api/changelog.rst
index fa349c2..5a1667d 100644
--- a/docs/api/changelog.rst
+++ b/docs/api/changelog.rst
@@ -6,6 +6,42 @@ All notable changes to this project will be documented in this file.
 The format is based on `Keep a Changelog`_, and this project adheres to
 `Semantic Versioning`_.
 
+Unreleased
+----------
+
+Fixed
+~~~~~
+
+- Previously, :class:`pandamesh.TriangleMesher` would not respect specified
+  cell sizes in areas that are fully bounded by linestrings (rather than
+  polygons), e.g. three separate lines forming a triangular zone. The reason is
+  that Triangle identifies such a zone as a separate region, and the point
+  specifying the maximum area is isolated. This has been fixed by checking
+  whether linestrings form any coincendental polygons, and including these
+  polygons are separate zones.
+
+Added
+~~~~~
+
+- :meth:`pandamesh.TriangleMesher.to_geodataframe()` and
+- :meth:`pandamesh.GmshMesher.to_geodataframe()` have been added to return
+  generated meshes as geodataframes.
+- Added :attr:`pandamesh.MeshAlgorithm.QUASI_STRUCTURED_QUAD` as an option.
+- Added :class:`pandamesh.Preprocessor` to assist in preparing and cleaning
+  geospatial data prior to meshing.
+
+Changed
+~~~~~~~
+
+- :class:`pandamesh.TriangleMesher` does a cell size to area conversion. This
+  previously assumed right-angled triangles. This has been changed to assume
+  equilateral triangles instead. This may result in slightly smaller triangles.
+- Mesher properties set with :class:`pandamesh.DelaunayAlgorithm`,
+  :class:`pandamesh.FieldCombination`, :class:`pandamesh.GeneralVerbosity`,
+  :class:`pandamesh.GmshMesher`, :class:`pandamesh.MeshAlgorithm`, or
+  :class:`pandamesh.SubdivisionAlgorithm` will now accept one of these enums,
+  or the enum member name as a string.
+
 [0.1.6] 2024-07-17
 ------------------
 
diff --git a/docs/api/gmsh.rst b/docs/api/gmsh.rst
deleted file mode 100644
index a9fcdf1..0000000
--- a/docs/api/gmsh.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Gmsh
-----
-
-.. automodule:: pandamesh.gmsh_mesher
-    :members:
-    :undoc-members:
diff --git a/docs/api/index.rst b/docs/api/index.rst
index 1195340..2980332 100644
--- a/docs/api/index.rst
+++ b/docs/api/index.rst
@@ -1,13 +1,94 @@
+.. currentmodule:: pandamesh
+
+.. _api:
+
 API Reference
 =============
 
+This page provides an auto-generated summary of pandamesh's API.
+
 .. toctree::
    :maxdepth: 1
 
    changelog
 
-.. toctree::
-    :maxdepth: 1
+Preprocessor
+------------
+
+.. autosummary::
+   :toctree: api/
+
+    Preprocessor
+    Preprocessor.unify_polygons
+    Preprocessor.merge_polygons
+    Preprocessor.clip_lines
+    Preprocessor.unify_lines
+    Preprocessor.interpolate_lines_to_points
+    Preprocessor.snap_points
+    Preprocessor.clip_points
+    Preprocessor.to_geodataframe
+
+Triangle
+--------
+
+.. autosummary::
+   :toctree: api/
+
+    TriangleMesher
+    TriangleMesher.generate
+    TriangleMesher.generate_geodataframe
+    TriangleMesher.generate_ugrid
+    TriangleMesher.minimum_angle
+    TriangleMesher.conforming_delaunay
+    TriangleMesher.suppress_exact_arithmetic
+    TriangleMesher.maximum_steiner_points
+    TriangleMesher.delaunay_algorithm
+    TriangleMesher.consistency_check
+
+Triangle Enumerators
+--------------------
+
+.. autosummary::
+   :toctree: api/
+   :template: enums.rst
+
+    DelaunayAlgorithm
+
+Gmsh
+----
+
+.. autosummary::
+   :toctree: api/
+
+    GmshMesher
+    GmshMesher.generate
+    GmshMesher.generate_geodataframe
+    GmshMesher.generate_ugrid
+    GmshMesher.mesh_algorithm
+    GmshMesher.recombine_all
+    GmshMesher.force_geometry
+    GmshMesher.mesh_size_extend_from_boundary
+    GmshMesher.mesh_size_from_points
+    GmshMesher.mesh_size_from_curvature
+    GmshMesher.field_combination
+    GmshMesher.subdivision_algorithm
+    GmshMesher.general_verbosity
+    GmshMesher.clear_fields
+    GmshMesher.add_distance_field
+    GmshMesher.add_structured_field
+    GmshMesher.write
+    GmshMesher.finalize_gmsh
+    gmsh_env
+
+Gmsh Enumerators
+----------------
+
+.. autosummary::
+   :toctree: api/
+   :template: enums.rst
 
-    triangle.rst
-    gmsh.rst
+    FieldCombination
+    GeneralVerbosity
+    GmshMesher
+    MeshAlgorithm
+    SubdivisionAlgorithm
diff --git a/docs/api/triangle.rst b/docs/api/triangle.rst
deleted file mode 100644
index 2906445..0000000
--- a/docs/api/triangle.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Triangle
---------
-
-.. automodule:: pandamesh.triangle_mesher
-    :members:
-    :undoc-members:
diff --git a/docs/conf.py b/docs/conf.py
index 8444076..75b2d9f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -29,6 +29,7 @@
 # ones.
 extensions = [
     "sphinx.ext.autodoc",
+    "sphinx.ext.autosummary",
     "sphinx.ext.viewcode",
     "sphinx.ext.todo",
     "sphinx.ext.napoleon",
@@ -66,7 +67,8 @@
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path .
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+templates_path = ["_templates"]
+exclude_patterns = ["_build", "_templates", "Thumbs.db", ".DS_Store"]
 
 # The name of the Pygments (syntax highlighting) style to use.
 pygments_style = "sphinx"
diff --git a/examples/preprocessing.py b/examples/preprocessing.py
new file mode 100644
index 0000000..79bc0e2
--- /dev/null
+++ b/examples/preprocessing.py
@@ -0,0 +1,389 @@
+"""
+Preprocessing
+=============
+
+Raw geospatial vector data is often not ready to use directly in mesh
+generation:
+    
+* Polygon data often do not form a valid planar partition: polygons are
+  overlapping, or neighboring polygons have small gaps between them.
+* Polygon boundaries or linestring segments intersect each other.
+* Points may be located on polygon boundaries or lines. Since floating point
+  numbers are not exact, points seemingly located on a line are computationally
+  just left or just right of the line and form an extremely thin triangle.
+* Points may be located extremely close together, thereby generating tiny
+  triangles.
+  
+Such problems either lead to a generated mesh with extremely small elements, or
+worse, they lead to a crash of the meshing program. Pandamesh provides a
+``Preprocessor`` class to assist with cleaning up some common faults. 
+
+This example will illustrate some common problems and how to resolve them.
+"""
+# %%
+import geopandas as gpd
+import matplotlib.pyplot as plt
+import numpy as np
+import shapely
+import shapely.geometry as sg
+
+import pandamesh as pm
+
+# %%
+# Polygons
+# --------
+#
+# When generating a mesh, we often have a general area which may be meshed
+# coarsely and an area of interest, which should be meshed more finely.
+# Generally, the fine inner zone is located within the coarse outer zone, but
+# this requires a hole in the outer zone that exactly matches up with the
+# exterior of the inner zone.
+
+outer = sg.Polygon(
+    [
+        [0.0, 0.0],
+        [10.0, 0.0],
+        [10.0, 10.0],
+        [0.0, 10.0],
+    ]
+)
+inner = sg.Polygon(
+    [
+        [5.0, 2.0],
+        [8.0, 5.0],
+        [5.0, 8.0],
+        [2.0, 5.0],
+    ]
+)
+
+gdf = gpd.GeoDataFrame(geometry=[outer, inner])
+gdf["cellsize"] = [2.0, 1.0]
+
+fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 5), sharex=True, sharey=True)
+gdf.iloc[[0]].plot(ax=ax0)
+gdf.iloc[[1]].plot(ax=ax1)
+
+# %%
+# In this case, we have two conflicting specified cell sizes in the inner
+# square. We can resolve this as follows:
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .unify_polygons()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+# %%
+# Note that the Preprocessor supports method chaining, allowing you to flexibly
+# execute a set of operations.
+#
+# The resulting geodataframe's geometries are valid planar partition:
+
+fig, (ax0, ax1) = plt.subplots(ncols=2, figsize=(10, 5), sharex=True, sharey=True)
+resolved.iloc[[0]].plot(ax=ax0)
+resolved.iloc[[1]].plot(ax=ax1)
+
+# %%
+# And we can use it directly to generate a mesh:
+
+vertices, faces = pm.TriangleMesher(resolved).generate()
+pm.plot(vertices, faces)
+
+# %%
+# Alternatively, multiple polygons with the same cell size specification might
+# be overlapping
+
+inner0 = shapely.affinity.translate(inner, xoff=-1.0)
+inner1 = shapely.affinity.translate(inner, xoff=1.0)
+gdf = gpd.GeoDataFrame(geometry=[outer, inner0, inner1])
+gdf["cellsize"] = [2.0, 1.0, 1.0]
+
+fig, ax = plt.subplots(figsize=(5, 5))
+gdf.plot(ax=ax, facecolor="none")
+# %%
+# These will also be resolved by ``.unify_polygons``.
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .unify_polygons()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.TriangleMesher(resolved).generate()
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+resolved.plot(ax=ax, facecolor="none", edgecolor="red")
+
+# %%
+# Note, however, that the internal boundaries of the inner polygons are forced
+# into the triangulation. We can rid of these by calling ``.merge_polygons``:
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .unify_polygons()
+    .merge_polygons()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.TriangleMesher(resolved).generate()
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+resolved.plot(ax=ax, facecolor="none", edgecolor="red")
+
+# %%
+# An alternative problem is when polygons are touching, but do not actually
+# share vertices along the boundary.
+
+first = sg.Polygon(
+    [
+        [0.0, 0.0],
+        [10.0, 0.0],
+        [10.0, 10.0],
+        [0.0, 10.0],
+    ]
+)
+second = sg.Polygon(
+    [
+        [10.0, 2.0],
+        [18.0, 2.0],
+        [18.0, 8.0],
+        [10.0, 8.0],
+    ]
+)
+
+gdf = gpd.GeoDataFrame(geometry=[first, second])
+gdf["cellsize"] = [4.0, 2.0]
+
+vertices, faces = pm.GmshMesher(gdf).generate()
+pm.plot(vertices, faces)
+
+# %%
+# At x=10.0, the generated triangles are disconnected.
+#
+# Calling ``.unify_polygons()`` ensures that the vertices of touching polygons
+# are inserted, such that the polygons share an edge.
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .unify_polygons()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.TriangleMesher(resolved).generate()
+polygon0_coords = shapely.get_coordinates(resolved.geometry[0])
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+ax.scatter(*polygon0_coords.T)
+
+# %%
+# Lines
+# -----
+#
+# Lines may only be only partially present, or present in holes:
+
+donut = sg.Polygon(
+    [
+        [0.0, 0.0],
+        [10.0, 0.0],
+        [10.0, 10.0],
+        [0.0, 10.0],
+    ],
+    holes=[
+        [
+            [2.0, 5.0],
+            [5.0, 8.0],
+            [8.0, 5.0],
+            [5.0, 2.0],
+        ]
+    ],
+)
+line0 = shapely.LineString(
+    [
+        [-2.0, 0.0],
+        [12.0, 10.0],
+    ]
+)
+line1 = shapely.LineString(
+    [
+        [5.5, 9.0],
+        [9.0, 5.5],
+    ]
+)
+
+gdf = gpd.GeoDataFrame(geometry=[donut, line0, line1])
+gdf["cellsize"] = [2.0, 1.0, 1.0]
+gdf.plot(edgecolor="k")
+
+# %%
+# A first step is to remove line segments that do not fall in any polygon:
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .clip_lines()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+resolved.plot(edgecolor="k")
+
+# %%
+# However, this doesn't create suitable input for meshing. The ``GmshMesher``
+# appears to hang on this input, and Triangle generates a grid with very small
+# triangles:
+
+vertices, faces = pm.TriangleMesher(resolved).generate()
+pm.plot(vertices, faces)
+
+# %%
+# A better approach here is to ensure all intersections are present in all
+# linework:
+#
+# * First we clip.
+# * Then we call ``unify_lines`` to ensure that the intersection of line0 and
+#   line1 at (7.625 6.875) is represented.
+# * Next we call ``unify_polygons``. This ensures the intersections of the lines
+#   with the poygon exterior is represented as well.
+# * The result of ``unify_polygons`` is that the line splits the polygon in two
+#   parts. These are merged back together with ``merge_polygons``.
+#
+# If we plot the vertices of the resolved polygon, we see that the intersection
+# vertices have been inserted into the polygon boundaries, and that the tiny
+# triangles around the line intersection have disappeared:
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .clip_lines()
+    .unify_lines()
+    .unify_polygons()
+    .merge_polygons()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.GmshMesher(resolved).generate()
+polygon0_coords = shapely.get_coordinates(resolved.geometry[0])
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+ax.scatter(*polygon0_coords.T)
+
+# %%
+# In some cases, having line segments terminate exactly on polygon boundaries
+# still causes trouble. We may also ensure that lines are some distance removed
+# from any polygon boundary by providing a distance to ``clip_lines``:
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .unify_lines()
+    .clip_lines(distance=0.5)
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.GmshMesher(resolved).generate()
+polygon0_coords = shapely.get_coordinates(resolved.geometry[0])
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+resolved.plot(facecolor="none", edgecolor="red", ax=ax)
+ax.scatter(*polygon0_coords.T)
+
+# %%
+# Another pragmatic approach is to convert any line into interpolated points.
+# Points cannot intersect each other, which sidesteps a large number of problems.
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .interpolate_lines_to_points(distance=0.25)
+    .clip_points()
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.GmshMesher(resolved).generate()
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+resolved.plot(facecolor="none", edgecolor="red", ax=ax)
+
+# %%
+# Points
+# ------
+#
+# Note that the start and end points of the lines are still on, or very near
+# the polygon edges.
+#
+# We can remove those points by providing a distance to ``clip_points``.
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .interpolate_lines_to_points(distance=0.25)
+    .clip_points(distance=0.5)
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.GmshMesher(resolved).generate()
+
+fig, ax = plt.subplots(figsize=(5, 5))
+pm.plot(vertices, faces, ax=ax)
+resolved.plot(facecolor="none", edgecolor="red", ax=ax)
+
+# %%
+# A problem with points is that they may be very close together, thereby
+# generating very small triangles. Let's generate 200 random points to illustrate:
+
+rng = np.random.default_rng()
+points = gpd.points_from_xy(*rng.random((2, 200)) * 10.0)
+gdf = gpd.GeoDataFrame(geometry=np.concatenate([[donut], points]))
+gdf["cellsize"] = 2.0
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .clip_points(distance=0.5)
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.GmshMesher(resolved).generate()
+pm.plot(vertices, faces)
+
+# %%
+# We can solve this by snapping points together that are located some distance
+# from each other:
+
+resolved = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .clip_points(distance=0.5)
+    .snap_points(distance=0.5)
+    .to_geodataframe()
+).rename(columns={"values": "cellsize"})
+
+vertices, faces = pm.GmshMesher(resolved).generate()
+pm.plot(vertices, faces)
+
+# %%
+# Flexibility and composability
+# -----------------------------
+#
+# The Preprocessor class in Pandamesh is designed with flexibility and
+# composability in mind through method chaining. By combining various
+# preprocessing steps in any order, you can address a wide range of geometric
+# issues. For instance, you might start by unifying polygons, then clip lines,
+# interpolate them to points, and finally snap those points together.
+#
+# The steps required depend on the nature of geometrical input, and may require
+# experimenting with various methods. The intermediate output can be checked
+# and visualized at any moments, by calling ``to_geodataframe``. For example,
+# to check the intermediate result after clipping but prior to snapping:
+
+check = (
+    pm.Preprocessor(geometry=gdf.geometry, values=gdf.cellsize)
+    .clip_points(distance=0.5)
+    .to_geodataframe()
+)
+
+check.plot(facecolor="none", edgecolor="red")
+
+# %%
+# This also makes it easy to apply the preprocessor in steps. Some steps may be
+# relatively costly, such as unifying a large number of detailed polygons. The
+# intermediate result can be stored as e.g. a GeoPackage. Then, in a separate
+# processing step, the intermediate result can be read again, and other
+# processing steps (such as filtering points) can be applied.
diff --git a/examples/triangle-geospatial.py b/examples/triangle-geospatial.py
index d49c2f4..3c5cd10 100644
--- a/examples/triangle-geospatial.py
+++ b/examples/triangle-geospatial.py
@@ -88,6 +88,11 @@
 pm.plot(vertices, triangles)
 
 # %%
+# For real work, buffering and simplifying will likely not suffice.
+#
+# See the preprocessing example to for an overview of common issues and how to
+# apply pandamesh's Preprocessor class to resolve them.
+#
 # Local refinement
 # ----------------
 #
diff --git a/pandamesh/__init__.py b/pandamesh/__init__.py
index 34bb31c..52fef40 100644
--- a/pandamesh/__init__.py
+++ b/pandamesh/__init__.py
@@ -1,14 +1,15 @@
 from pandamesh import data
-from pandamesh.gmsh_mesher import (
+from pandamesh.gmsh_enums import (
     FieldCombination,
     GeneralVerbosity,
-    GmshMesher,
     MeshAlgorithm,
     SubdivisionAlgorithm,
-    gmsh_env,
 )
+from pandamesh.gmsh_mesher import GmshMesher, gmsh_env
 from pandamesh.plot import plot
-from pandamesh.triangle_mesher import DelaunayAlgorithm, TriangleMesher
+from pandamesh.preprocessor import Preprocessor
+from pandamesh.triangle_enums import DelaunayAlgorithm
+from pandamesh.triangle_mesher import TriangleMesher
 
 __version__ = "0.1.6"
 
@@ -24,4 +25,5 @@
     "plot",
     "DelaunayAlgorithm",
     "TriangleMesher",
+    "Preprocessor",
 )
diff --git a/pandamesh/common.py b/pandamesh/common.py
index 82a5e90..a413535 100644
--- a/pandamesh/common.py
+++ b/pandamesh/common.py
@@ -2,15 +2,16 @@
 
 import functools
 import operator
-from enum import Enum, IntEnum
-from itertools import combinations
 from typing import Any, Sequence, Tuple
 
 import geopandas as gpd
 import numpy as np
+import pandas as pd
+import shapely
+from pandas.api.types import is_integer_dtype
 
 
-class MaybeGmsh:
+class MaybeGmsh:  # pragma: no cover
     """Gmsh is an optional dependency."""
 
     def __init__(self):
@@ -39,8 +40,10 @@ def __getattr__(self, name: str):
 
 
 gmsh = MaybeGmsh()
+BoolArray = np.ndarray
 IntArray = np.ndarray
 FloatArray = np.ndarray
+GeometryArray = np.ndarray
 coord_dtype = np.dtype([("x", np.float64), ("y", np.float64)])
 
 
@@ -61,12 +64,20 @@ def flatten(seq: Sequence[Any]):
     return functools.reduce(operator.concat, seq)
 
 
-def _show_options(options: Enum) -> str:
-    return "\n".join(map(str, options))
+def flatten_geometry(geom):
+    """Recursively flatten geometry collections."""
+    if hasattr(geom, "geoms"):
+        return [g for subgeom in geom.geoms for g in flatten_geometry(subgeom)]
+    else:
+        return [geom]
 
 
-def invalid_option(value: Any, options: Enum | IntEnum) -> str:
-    return f"Invalid option: {value}. Valid options are:\n{_show_options(options)}"
+def flatten_geometries(geometries: Sequence) -> GeometryArray:
+    """Flatten geometry collections."""
+    flattened = []
+    for geom in geometries:
+        flattened.extend(flatten_geometry(geom))
+    return np.array(flattened)
 
 
 def check_geodataframe(features: gpd.GeoDataFrame) -> None:
@@ -79,7 +90,7 @@ def check_geodataframe(features: gpd.GeoDataFrame) -> None:
         raise ValueError(f'Missing column "cellsize" in columns: {colnames}')
     if len(features) == 0:
         raise ValueError("Dataframe is empty")
-    if not features.index.is_integer():
+    if not is_integer_dtype(features.index):
         raise ValueError(
             f"geodataframe index is not integer typed, received: {features.index.dtype}"
         )
@@ -87,50 +98,16 @@ def check_geodataframe(features: gpd.GeoDataFrame) -> None:
         raise ValueError("geodataframe index contains duplicates")
 
 
-def overlap_shortlist(features: gpd.GeoSeries) -> Tuple[IntArray, IntArray]:
-    """
-    Create a shortlist of polygons or linestrings indices to check against each
-    other using their bounding boxes.
-    """
-    bounds = features.bounds
-    index_a, index_b = (
-        np.array(index) for index in zip(*combinations(features.index, 2))
-    )
-    df_a = bounds.loc[index_a]
-    df_b = bounds.loc[index_b]
-    # Convert to dict to get rid of clashing index.
-    a = {k: df_a[k].to_numpy() for k in df_a}
-    b = {k: df_b[k].to_numpy() for k in df_b}
-    # Touching does not count as overlap here.
-    overlap = (
-        (a["maxx"] >= b["minx"])
-        & (b["maxx"] >= a["minx"])
-        & (a["maxy"] >= b["miny"])
-        & (b["maxy"] >= a["miny"])
-    )
-    return index_a[overlap], index_b[overlap]
-
-
 def intersecting_features(features, feature_type) -> Tuple[IntArray, IntArray]:
-    # Check all combinations where bounding boxes overlap.
-    index_a, index_b = overlap_shortlist(features)
-    unique = np.unique(np.concatenate([index_a, index_b]))
-
-    # Now do the expensive intersection check.
-    # Polygons that touch are allowed, but they result in intersects() == True.
-    # To avoid this, we create temporary geometries that are slightly smaller
-    # by buffering with a small negative value.
-    shortlist = features.loc[unique]
-    if feature_type == "polygon":
-        shortlist = shortlist.buffer(-1.0e-6)
-    a = shortlist.loc[index_a]
-    b = shortlist.loc[index_b]
-    # Synchronize index so there's a one to one (row to row) intersection
-    # check.
-    a.index = np.arange(len(a))
-    b.index = np.arange(len(b))
-    with_overlap = a.intersects(b).to_numpy()
-    return index_a[with_overlap], index_b[with_overlap]
+    tree = shapely.STRtree(geoms=features)
+    if feature_type == "polygon":  # TODO: might not be necessary
+        target = features.buffer(-1.0e-6)
+    else:
+        target = features
+    i, j = tree.query(geometry=target, predicate="intersects")
+    # Intersection matrix is symmetric, and contains i==j (diagonal)
+    keep = j > i
+    return i[keep], j[keep]
 
 
 def check_intersection(features: gpd.GeoSeries, feature_type: str) -> None:
@@ -214,7 +191,11 @@ def separate(
     geom_type = gdf.geom_type
     acceptable = ["Polygon", "LineString", "Point"]
     if not geom_type.isin(acceptable).all():
-        raise TypeError(f"Geometry should be one of {acceptable}")
+        raise TypeError(
+            f"Geometry should be one of {acceptable}. "
+            "Call geopandas.GeoDataFrame.explode() to explode multi-part "
+            "geometries into multiple single geometries."
+        )
 
     polygons = gdf.loc[geom_type == "Polygon"].copy()
     linestrings = gdf.loc[geom_type == "LineString"].copy()
@@ -231,7 +212,7 @@ def separate(
     return polygons, linestrings, points
 
 
-def to_ugrid(vertices: FloatArray, faces: IntArray) -> "xugrid.Ugrid2d":  # type: ignore # noqa
+def to_ugrid(vertices: FloatArray, faces: IntArray) -> "xugrid.Ugrid2d":  # type: ignore # noqa pragma: no cover
     try:
         import xugrid
     except ImportError:
@@ -239,3 +220,57 @@ def to_ugrid(vertices: FloatArray, faces: IntArray) -> "xugrid.Ugrid2d":  # type
             "xugrid must be installed to return generated result a xugrid.Ugrid2d"
         )
     return xugrid.Ugrid2d(*vertices.T, -1, faces)
+
+
+def to_geodataframe(vertices: FloatArray, faces: IntArray) -> gpd.GeoDataFrame:
+    n_face, n_vertex = faces.shape
+    if n_vertex == 3:  # no fill values
+        coordinates = vertices[faces]
+        geometry = shapely.polygons(coordinates)
+    else:  # Possible fill values (-1)
+        # Group them by number of vertices.
+        valid = faces >= 0
+        n_valid = valid.sum(axis=1)
+        grouped_by_n_vertex = pd.DataFrame(
+            {"i": np.arange(n_face), "n": n_valid}
+        ).groupby("n")
+        geometries = []
+        for n_vertex, group in grouped_by_n_vertex:
+            group_faces = faces[group["i"], :n_vertex]
+            group_coordinates = vertices[group_faces]
+            geometries.append(shapely.polygons(group_coordinates))
+        geometry = np.concatenate(geometries)
+    return gpd.GeoDataFrame(geometry=geometry)
+
+
+class Grouper:
+    """
+    Wrapper around pd.DataFrame().groupby().
+
+    Group by ``a_index``, then iterates over the groups, returning a single
+    value of ``a`` and potentially multiple values for ``b``.
+    """
+
+    def __init__(
+        self,
+        a,
+        a_index,
+        b,
+        b_index,
+    ):
+        self.a = np.asarray(a)
+        self.b = np.asarray(b)
+        self.grouped = iter(
+            pd.DataFrame(data={"a": a_index, "b": b_index}).groupby("a")
+        )
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        try:
+            a_index, group = next(self.grouped)
+            b_index = group["b"]
+            return self.a[a_index], self.b[b_index]
+        except StopIteration:
+            raise StopIteration
diff --git a/pandamesh/enum_base.py b/pandamesh/enum_base.py
new file mode 100644
index 0000000..9ed8643
--- /dev/null
+++ b/pandamesh/enum_base.py
@@ -0,0 +1,26 @@
+from enum import Enum
+from typing import Type, TypeVar, Union
+
+E = TypeVar("E", bound="FlexibleEnum")
+
+
+def _show_options(options: Type[E]) -> str:
+    return "\n * ".join(map(str, options.__members__))
+
+
+class FlexibleEnum(Enum):
+    @classmethod
+    def from_value(cls: Type[E], value: Union[E, str]) -> E:
+        if isinstance(value, cls):
+            return value
+        elif isinstance(value, str):
+            try:
+                return cls.__members__[value]
+            except KeyError:
+                pass
+
+        raise ValueError(
+            # Use __repr__() so strings are shown with quotes.
+            f"{value.__repr__()} is not a valid {cls.__name__}. "
+            f"Valid options are:\n * {_show_options(cls)}"
+        )
diff --git a/pandamesh/gmsh_enums.py b/pandamesh/gmsh_enums.py
new file mode 100644
index 0000000..4d3dadc
--- /dev/null
+++ b/pandamesh/gmsh_enums.py
@@ -0,0 +1,166 @@
+from pandamesh.enum_base import FlexibleEnum
+
+
+class MeshAlgorithm(FlexibleEnum):
+    """
+    Gmsh meshing algorithm. Each algorithm has its own advantages and
+    disadvantages.
+
+    For all 2D unstructured algorithms a Delaunay mesh that contains all
+    the points of the 1D mesh is initially constructed using a
+    divide-and-conquer algorithm. Missing edges are recovered using edge
+    swaps. After this initial step several algorithms can be applied to
+    generate the final mesh:
+
+    * The MeshAdapt algorithm is based on local mesh modifications. This
+      technique makes use of edge swaps, splits, and collapses: long edges
+      are split, short edges are collapsed, and edges are swapped if a
+      better geometrical configuration is obtained.
+    * The Delaunay algorithm is inspired by the work of the GAMMA team at
+      INRIA. New points are inserted sequentially at the circumcenter of
+      the element that has the largest adimensional circumradius. The mesh
+      is then reconnected using an anisotropic Delaunay criterion.
+    * The Frontal-Delaunay algorithm is inspired by the work of S. Rebay.
+    * Other experimental algorithms with specific features are also
+      available. In particular, Frontal-Delaunay for Quads is a variant of
+      the Frontal-Delaunay algorithm aiming at generating right-angle
+      triangles suitable for recombination; and BAMG allows to generate
+      anisotropic triangulations.
+
+    For very complex curved surfaces the MeshAdapt algorithm is the most robust.
+    When high element quality is important, the Frontal-Delaunay algorithm should
+    be tried. For very large meshes of plane surfaces the Delaunay algorithm is
+    the fastest; it usually also handles complex mesh size fields better than the
+    Frontal-Delaunay. When the Delaunay or Frontal-Delaunay algorithms fail,
+    MeshAdapt is automatically triggered. The Automatic algorithm uses
+    Delaunay for plane surfaces and MeshAdapt for all other surfaces.
+    """
+
+    MESH_ADAPT = 1
+    """
+    Local mesh modifications using edge swaps, splits, and collapses. Robust
+    for complex curved surfaces.
+    """
+
+    AUTOMATIC = 2
+    """
+    Uses Delaunay for plane surfaces and MeshAdapt for all other surfaces.
+    """
+
+    INITIAL_MESH_ONLY = 3
+    """Generates only the initial Delaunay triangulation."""
+
+    FRONTAL_DELAUNAY = 5
+    """Good for high element quality."""
+
+    BAMG = 7
+    """Experimental algorithm for generating anisotropic triangulations."""
+
+    FRONTAL_DELAUNAY_FOR_QUADS = 8
+    """
+    Variant of Frontal-Delaunay aiming to generate right-angle triangles
+    suitable for recombination.
+    """
+
+    PACKING_OF_PARALLELLOGRAMS = 9
+    """Experimental algorithm for parallelogram-based mesh generation."""
+
+    QUASI_STRUCTURED_QUAD = 11
+    """
+    Combines an initial unstructured quad mesh with topological improvements
+    guided by cross fields to produce quasi-structured meshes with few
+    irregular vertices.
+    """
+
+
+class SubdivisionAlgorithm(FlexibleEnum):
+    """
+    Controls how Gmsh recombines triangles to form quads.
+
+    The default recombination algorithm might leave some triangles in the mesh,
+    if recombining all the triangles leads to badly shaped quads. In such
+    cases, to generate full-quad meshes, you can either subdivide the resulting
+    hybrid mesh (ALL_QUADRANGLES), or use the full-quad recombination
+    algorithm, which will automatically perform a coarser mesh followed by
+    recombination, smoothing and subdivision.
+    """
+
+    NONE = 0
+    """
+    No subdivision is applied. The mesh remains as is after the initial
+    recombination process, potentially leaving some triangles in the mesh.
+    """
+    ALL_QUADRANGLES = 1
+    """
+    Subdivides the mesh to convert all elements into quadrangles. This method
+    ensures a full-quad mesh by subdividing any remaining triangles after the
+    initial recombination process.
+    """
+    BARYCENTRIC = 3
+    """
+    Applies barycentric subdivision to the mesh. This method subdivides each
+    element by connecting its barycenter to the midpoints of its edges,
+    resulting in a refined mesh with increased element count.
+    """
+
+
+class FieldCombination(FlexibleEnum):
+    """
+    Controls how cell size fields are combined in Gmsh when they are found at
+    the same location.
+    """
+
+    MIN = "Min"
+    """Use the minimum size."""
+    MAX = "Max"
+    """Use the maximum size."""
+    MEAN = "Mean"
+    """Use the mean size."""
+
+
+class GeneralVerbosity(FlexibleEnum):
+    """Gmsh level of information printed."""
+
+    SILENT = 0
+    """No output is printed. All messages are suppressed."""
+
+    ERRORS = 1
+    """
+    Only error messages are printed. This level is useful when you want to be
+    alerted only to critical issues that prevent correct execution.
+    """
+
+    WARNINGS = 2
+    """
+    Error and warning messages are printed. This level adds important
+    cautionary information that doesn't necessarily prevent execution but might
+    affect results.
+    """
+
+    DIRECT = 3
+    """
+    Errors, warnings, and direct output from Gmsh commands are printed. This
+    level is useful for seeing immediate results of operations without too much
+    extra information.
+    """
+
+    INFORMATION = 4
+    """
+    Errors, warnings, direct output, and additional informational messages are
+    printed. This level provides more detailed feedback about the progress and
+    state of operations.
+    """
+
+    STATUS = 5
+    """
+    Errors, warnings, direct output, information, and status updates are
+    printed. This level offers comprehensive feedback, including progress
+    indicators for longer operations.
+    """
+
+    DEBUG = 99
+    """
+    All possible output is printed, including detailed debugging information.
+    This level is extremely verbose and is typically used for troubleshooting
+    or development purposes.
+    """
diff --git a/pandamesh/gmsh_fields.py b/pandamesh/gmsh_fields.py
index a16cc76..c4d321b 100644
--- a/pandamesh/gmsh_fields.py
+++ b/pandamesh/gmsh_fields.py
@@ -41,7 +41,7 @@ def write_structured_field_file(
         Writes a structured gmsh field file.
     """
     shape = cellsize.shape
-    if cellsize.ndim() != 2:
+    if cellsize.ndim != 2:
         raise ValueError(f"`cellsize` must be 2D. Received an array of shape: {shape}")
     nrow, ncol = shape
     # Flip values around if dx or dy is negative.
diff --git a/pandamesh/gmsh_geometry.py b/pandamesh/gmsh_geometry.py
index a8b31d2..c4c53ac 100644
--- a/pandamesh/gmsh_geometry.py
+++ b/pandamesh/gmsh_geometry.py
@@ -169,6 +169,8 @@ def embed_where(gdf: gpd.GeoDataFrame, polygons: gpd.GeoDataFrame) -> gpd.GeoDat
 def add_geometry(
     polygons: gpd.GeoDataFrame, linestrings: gpd.GeoDataFrame, points: gpd.GeoDataFrame
 ):
+    if len(polygons) == 0:
+        raise ValueError("No polygons provided")
     # Assign unique ids
     polygons["__polygon_id"] = np.arange(1, len(polygons) + 1)
 
diff --git a/pandamesh/gmsh_mesher.py b/pandamesh/gmsh_mesher.py
index 5408298..add116c 100644
--- a/pandamesh/gmsh_mesher.py
+++ b/pandamesh/gmsh_mesher.py
@@ -2,7 +2,6 @@
 import pathlib
 import tempfile
 from contextlib import contextmanager
-from enum import Enum, IntEnum
 from typing import List, Tuple, Union
 
 import geopandas as gpd
@@ -14,10 +13,14 @@
     IntArray,
     check_geodataframe,
     gmsh,
-    invalid_option,
     repr,
     separate,
-    to_ugrid,
+)
+from pandamesh.gmsh_enums import (
+    FieldCombination,
+    GeneralVerbosity,
+    MeshAlgorithm,
+    SubdivisionAlgorithm,
 )
 from pandamesh.gmsh_fields import (
     FIELDS,
@@ -26,6 +29,7 @@
     validate_field,
 )
 from pandamesh.gmsh_geometry import add_field_geometry, add_geometry
+from pandamesh.mesher_base import MesherBase
 
 
 @contextmanager
@@ -41,80 +45,6 @@ def gmsh_env(read_config_files: bool = True, interruptible: bool = True):
         gmsh.finalize()
 
 
-class MeshAlgorithm(IntEnum):
-    """
-    Each algorithm has its own advantages and disadvantages.
-
-    For all 2D unstructured algorithms a Delaunay mesh that contains all
-    the points of the 1D mesh is initially constructed using a
-    divide-and-conquer algorithm. Missing edges are recovered using edge
-    swaps. After this initial step several algorithms can be applied to
-    generate the final mesh:
-
-    * The MeshAdapt algorithm is based on local mesh modifications. This
-      technique makes use of edge swaps, splits, and collapses: long edges
-      are split, short edges are collapsed, and edges are swapped if a
-      better geometrical configuration is obtained.
-    * The Delaunay algorithm is inspired by the work of the GAMMA team at
-      INRIA. New points are inserted sequentially at the circumcenter of
-      the element that has the largest adimensional circumradius. The mesh
-      is then reconnected using an anisotropic Delaunay criterion.
-    * The Frontal-Delaunay algorithm is inspired by the work of S. Rebay.
-    * Other experimental algorithms with specific features are also
-      available. In particular, Frontal-Delaunay for Quads is a variant of
-      the Frontal-Delaunay algorithm aiming at generating right-angle
-      triangles suitable for recombination; and BAMG allows to generate
-      anisotropic triangulations.
-
-    For very complex curved surfaces the MeshAdapt algorithm is the most robust.
-    When high element quality is important, the Frontal-Delaunay algorithm should
-    be tried. For very large meshes of plane surfaces the Delaunay algorithm is
-    the fastest; it usually also handles complex mesh size fields better than the
-    Frontal-Delaunay. When the Delaunay or Frontal-Delaunay algorithms fail,
-    MeshAdapt is automatically triggered. The Automatic algorithm uses
-    Delaunay for plane surfaces and MeshAdapt for all other surfaces.
-    """
-
-    MESH_ADAPT = 1
-    AUTOMATIC = 2
-    INITIAL_MESH_ONLY = 3
-    FRONTAL_DELAUNAY = 5
-    BAMG = 7
-    FRONTAL_DELAUNAY_FOR_QUADS = 8
-    PACKING_OF_PARALLELLOGRAMS = 9
-
-
-class SubdivisionAlgorithm(IntEnum):
-    """All meshes can be subdivided to generate fully quadrangular cells."""
-
-    NONE = 0
-    ALL_QUADRANGLES = 1
-    BARYCENTRIC = 3
-
-
-class FieldCombination(Enum):
-    """
-    Controls how cell size fields are combined when they are found at the
-    same location.
-    """
-
-    MIN = "Min"
-    MAX = "Max"
-    MEAN = "Mean"
-
-
-class GeneralVerbosity(IntEnum):
-    """Level of information printed."""
-
-    SILENT = 0
-    ERRORS = 1
-    WARNINGS = 2
-    DIRECT = 3
-    INFORMATION = 4
-    STATUS = 5
-    DEBUG = 99
-
-
 def coerce_field(field: Union[dict, str]) -> dict:
     if not isinstance(field, (dict, str)):
         raise TypeError("field must be a dictionary or a valid JSON dictionary string")
@@ -123,14 +53,14 @@ def coerce_field(field: Union[dict, str]) -> dict:
     return field
 
 
-class GmshMesher:
+class GmshMesher(MesherBase):
     """
     Wrapper for the python bindings to Gmsh. This class must be initialized
     with a geopandas GeoDataFrame containing at least one polygon, and a column
     named ``"cellsize"``.
 
     Optionally, multiple polygons with different cell sizes can be included in
-    the geodataframe. These can be used to achieve local mesh remfinement.
+    the geodataframe. These can be used to achieve local mesh refinement.
 
     Linestrings and points may also be included. The segments of linestrings
     will be directly forced into the triangulation. Points can also be forced
@@ -142,7 +72,8 @@ class GmshMesher:
     the geodataframe are checked:
 
         * Polygons should not have any overlap with each other.
-        * Linestrings should not intersect each other.
+        * Linestrings should not intersect each other, unless the intersection
+          vertex is present in both.
         * Every linestring should be fully contained by a single polygon;
           a linestring may not intersect two or more polygons.
         * Linestrings and points should not "touch" / be located on
@@ -150,7 +81,8 @@ class GmshMesher:
         * Holes in polygons are fully supported, but they must not contain
           any linestrings or points.
 
-    If such cases are detected, the initialization will error.
+    If such cases are detected, the initialization will error: use the
+    :class:`pandamesh.Preprocessor` to clean up geometries beforehand.
 
     For more details on Gmsh, see:
     https://gmsh.info/doc/texinfo/gmsh.html
@@ -195,7 +127,6 @@ def __init__(
         # Set default values for meshing parameters
         self.mesh_algorithm = MeshAlgorithm.AUTOMATIC
         self.recombine_all = False
-        # self.force_geometry = True  # not implemented yet, see below
         self.mesh_size_extend_from_boundary = True
         self.mesh_size_from_points = True
         self.mesh_size_from_curvature = False
@@ -224,7 +155,7 @@ def finalize_gmsh():
     # Properties
     # ----------
     @property
-    def mesh_algorithm(self):
+    def mesh_algorithm(self) -> MeshAlgorithm:
         """
         Can be set to one of :py:class:`pandamesh.MeshAlgorithm`:
 
@@ -237,19 +168,19 @@ def mesh_algorithm(self):
             BAMG = 7
             FRONTAL_DELAUNAY_FOR_QUADS = 8
             PACKING_OF_PARALLELLOGRAMS = 9
+            QUASI_STRUCTURED_QUAD = 11
 
         Each algorithm has its own advantages and disadvantages.
         """
-        return gmsh.option.getNumber("Mesh.Algorithm")
+        return MeshAlgorithm(gmsh.option.getNumber("Mesh.Algorithm"))
 
     @mesh_algorithm.setter
     def mesh_algorithm(self, value: MeshAlgorithm):
-        if value not in MeshAlgorithm:
-            raise ValueError(invalid_option(value, MeshAlgorithm))
+        value = MeshAlgorithm.from_value(value)
         gmsh.option.setNumber("Mesh.Algorithm", value.value)
 
     @property
-    def recombine_all(self):
+    def recombine_all(self) -> bool:
         """
         Apply recombination algorithm to all surfaces, ignoring per-surface
         spec.
@@ -274,7 +205,7 @@ def force_geometry(self, value: bool) -> None:
         self._force_geometry = value
 
     @property
-    def mesh_size_extend_from_boundary(self):
+    def mesh_size_extend_from_boundary(self) -> bool:
         """
         Forces the mesh size to be extended from the boundary, or not, per
         surface.
@@ -304,7 +235,7 @@ def mesh_size_from_points(self, value):
     def mesh_size_from_curvature(self):
         """
         Automatically compute mesh element sizes from curvature, using the value as
-        the target number of elements per 2 * Pi radians
+        the target number of elements per 2 * Pi radians.
         """
         return self._mesh_size_from_curvature
 
@@ -320,7 +251,7 @@ def mesh_size_from_curvature(self, value):
         gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", value)
 
     @property
-    def field_combination(self):
+    def field_combination(self) -> FieldCombination:
         """
         Controls how cell size fields are combined when they are found at the
         same location. Can be set to one of
@@ -336,13 +267,13 @@ def field_combination(self):
         return self._field_combination
 
     @field_combination.setter
-    def field_combination(self, value):
-        if value not in FieldCombination:
-            raise ValueError(invalid_option(value, FieldCombination))
+    def field_combination(self, value: Union[FieldCombination, str]):
+        value = FieldCombination.from_value(value)
         self._field_combination = value
+        # Value is propagated to gmsh in ._combine_fields()
 
     @property
-    def subdivision_algorithm(self):
+    def subdivision_algorithm(self) -> SubdivisionAlgorithm:
         """
         All meshes can be subdivided to generate fully quadrangular cells. Can
         be set to one of :py:class:`pandamesh.SubdivisionAlgorithm`:
@@ -354,25 +285,37 @@ def subdivision_algorithm(self):
             BARYCENTRIC = 3
 
         """
-        return self._subdivision_algorithm
+
+        return SubdivisionAlgorithm(gmsh.option.getNumber("Mesh.SubdivisionAlgorithm"))
 
     @subdivision_algorithm.setter
-    def subdivision_algorithm(self, value):
-        if value not in SubdivisionAlgorithm:
-            raise ValueError(invalid_option(value, SubdivisionAlgorithm))
-        self._subdivision_algorithm = value
-        gmsh.option.setNumber("Mesh.SubdivisionAlgorithm", value)
+    def subdivision_algorithm(self, value: Union[SubdivisionAlgorithm, str]):
+        value = SubdivisionAlgorithm.from_value(value)
+        gmsh.option.setNumber("Mesh.SubdivisionAlgorithm", value.value)
 
     @property
     def general_verbosity(self) -> GeneralVerbosity:
-        return self._general_verbosity
+        """
+        Controls level of information printed. Can be set to one of
+        :py:class:`pandamesh.GeneralVerbosity`:
+
+        .. code::
+
+            SILENT = 0
+            ERRORS = 1
+            WARNINGS = 2
+            DIRECT = 3
+            INFORMATION = 4
+            STATUS = 5
+            DEBUG = 99
+
+        """
+        return GeneralVerbosity(gmsh.option.getNumber("General.Verbosity"))
 
     @general_verbosity.setter
-    def general_verbosity(self, value: GeneralVerbosity) -> None:
-        if value not in GeneralVerbosity:
-            raise ValueError(invalid_option(value, GeneralVerbosity))
-        self._general_verbosity = value
-        gmsh.option.setNumber("General.Verbosity", value)
+    def general_verbosity(self, value: Union[GeneralVerbosity, str]) -> None:
+        value = GeneralVerbosity.from_value(value)
+        gmsh.option.setNumber("General.Verbosity", value.value)
 
     # Methods
     # -------
@@ -549,9 +492,6 @@ def generate(self) -> Tuple[FloatArray, IntArray]:
 
         return self._vertices(), self._faces()
 
-    def generate_ugrid(self) -> "xugrid.Ugrid2d":  # type: ignore # noqa
-        return to_ugrid(*self.generate())
-
     def write(self, path: Union[str, pathlib.Path]):
         """
         Write a gmsh .msh file
@@ -560,4 +500,4 @@ def write(self, path: Union[str, pathlib.Path]):
         ----------
         path: Union[str, pathlib.Path
         """
-        gmsh.write(path)
+        gmsh.write(str(path))
diff --git a/pandamesh/mesher_base.py b/pandamesh/mesher_base.py
new file mode 100644
index 0000000..26fbc97
--- /dev/null
+++ b/pandamesh/mesher_base.py
@@ -0,0 +1,32 @@
+import abc
+from typing import Tuple
+
+import geopandas as gpd
+
+from pandamesh.common import FloatArray, IntArray, to_geodataframe, to_ugrid
+
+
+class MesherBase(abc.ABC):
+    @abc.abstractmethod
+    def generate(self) -> Tuple[FloatArray, IntArray]:
+        pass
+
+    def generate_geodataframe(self) -> gpd.GeoDataFrame:
+        """
+        Generate a mesh and return it as a geopandas GeoDataFrame.
+
+        Returns
+        -------
+        mesh: geopandas.GeoDataFrame
+        """
+        return to_geodataframe(*self.generate())
+
+    def generate_ugrid(self) -> "xugrid.Ugrid2d":  # type: ignore # noqa  pragma: no cover
+        """
+        Generate a mesh and return it as an xugrid Ugrid2d.
+
+        Returns
+        -------
+        mesh: xugrid.Ugrid2d
+        """
+        return to_ugrid(*self.generate())
diff --git a/pandamesh/preprocessor.py b/pandamesh/preprocessor.py
new file mode 100644
index 0000000..82e1bca
--- /dev/null
+++ b/pandamesh/preprocessor.py
@@ -0,0 +1,551 @@
+from typing import Optional, Tuple
+
+import geopandas as gpd
+import numpy as np
+import pandas as pd
+import shapely
+from shapely import STRtree
+
+from pandamesh.common import (
+    BoolArray,
+    GeometryArray,
+    IntArray,
+    flatten_geometries,
+    flatten_geometry,
+)
+from pandamesh.snapping import snap_nodes
+
+
+def collect_exteriors(geometry: GeometryArray) -> GeometryArray:
+    return shapely.get_exterior_ring(geometry)
+
+
+def collect_interiors(geometry: GeometryArray) -> GeometryArray:
+    interiors = []
+    for geom in geometry:
+        interiors.extend(geom.interiors)
+    return np.asarray(interiors)
+
+
+def filter_holes_and_assign_values(
+    polygons: GeometryArray,
+    index_original: IntArray,
+    index_union: IntArray,
+    indexer: np.ndarray,
+    ascending: bool,
+) -> Tuple[BoolArray, np.ndarray]:
+    df = pd.DataFrame(
+        data={
+            "union": index_union,
+            "original": index_original,
+            "indexer": indexer[index_original],
+        }
+    )
+    result = (
+        df.sort_values("indexer", ascending=ascending)
+        .groupby("union", as_index=False)
+        .first()
+    )
+    maintain = np.full(len(polygons), False)
+    maintain[df["union"]] = True
+    return polygons[maintain], result["indexer"].to_numpy()
+
+
+def locate_polygons(
+    new: GeometryArray,
+    old: GeometryArray,
+    indexer: IntArray,
+    ascending: bool,
+) -> Tuple[GeometryArray, IntArray]:
+    # new is the partioned unary union of old. Every linear ring will be a
+    # polygon, this includes holes. Grab a point inside of new, then find out
+    # where they are located (in which polygon) or even outside (in a hole).
+    sampling_points = [polygon.representative_point() for polygon in new]
+    tree = STRtree(old)
+    index_union, index_original = tree.query(sampling_points, predicate="within")
+    # In case of overlaps, there are multiple answers for each union polygon.
+    new_polygons, new_indexer = filter_holes_and_assign_values(
+        polygons=new,
+        index_original=index_original,
+        index_union=index_union,
+        indexer=indexer,
+        ascending=ascending,
+    )
+    return new_polygons, new_indexer
+
+
+def locate_lines(
+    new: GeometryArray,
+    old: GeometryArray,
+    indexer: IntArray,
+) -> IntArray:
+    sampling_points = shapely.line_interpolate_point(new, distance=0.5, normalized=True)
+    tree = STRtree(old)
+    index_old = tree.nearest(sampling_points)
+    return indexer[index_old]
+
+
+def merge_polygons(geometry: GeometryArray, grid_size) -> GeometryArray:
+    merged = shapely.unary_union(geometry, grid_size=grid_size)
+    return np.asarray(flatten_geometry(merged))
+
+
+def separate(
+    geometry: GeometryArray,
+) -> Tuple[GeometryArray, GeometryArray, GeometryArray]:
+    type_id = shapely.get_type_id(geometry)
+    acceptable = {"Point", "LineString", "LinearRing", "Polygon"}
+    if not np.isin(type_id, (0, 1, 2, 3)).all():
+        raise TypeError(
+            f"Geometry should be one of {acceptable}. "
+            "Call geopandas.GeoDataFrame.explode() to explode multi-part "
+            "geometries into multiple single geometries."
+        )
+    return (
+        geometry[type_id == 3],
+        geometry[(type_id == 1) | (type_id == 2)],
+        geometry[type_id == 0],
+    )
+
+
+class Preprocessor:
+    """
+    Utilities to clean-up geometries before meshing.
+
+    Each processing method return a new Preprocessor instance. This enables
+    method chaining.
+
+    Note: many methods require exhaustive checking of geometries. Processing
+    may be slow if your geometry contains thousands of geometries or if it has
+    excessive detail.
+
+    Parameters
+    ----------
+    geometry: np.ndarray of shapely geometries of length N.
+        Should be points, linestrings, or polygons.
+    values: np.ndarray of length N, optional
+        Values associated with the geometries (e.g. cell sizes for meshing).
+    grid_size: float, optional
+        Forwarded to shapely operations. If grid_size is nonzero, input
+        coordinates will be snapped to a precision grid of that size and
+        resulting coordinates will be snapped to that same grid for shapely
+        operations such as ``intersection, difference, union``, etc. If 0, the
+        operations will use double precision coordinates.
+
+    Examples
+    --------
+    This class allows for method chaining to flexibly combine pre-processing
+    operations:
+
+    >>> processed = (
+    >>>     Preprocessor(geometry=gdf.geometry)
+    >>>     .interpolate_lines_to_points(distance=5.0)
+    >>>     .snap_points(distance=2.0)
+    >>>     .unify_polygons()
+    >>>     .merge_polygons()
+    >>>     .clip_points(distance=5.0)
+    >>>     .to_geodataframe()
+    >>> )
+
+    An intermediate result can be checked and inspect by calling ``.to_geodataframe()``
+    at any time.
+
+    >>> check = Preprocessor(geometry=gdf.geometry).interpolate_lines_to_points(5.0).to_geodataframe()
+    """
+
+    def __init__(
+        self,
+        geometry=None,
+        values=None,
+        grid_size=None,
+    ):
+        geometry = np.asarray(geometry)
+        self.polygons, self.lines, self.points = separate(geometry)
+        if values is not None:
+            values = np.asarray(values)
+            v_shape = values.shape
+            g_shape = geometry.shape
+            if v_shape != g_shape:
+                raise ValueError(
+                    f"geometry and values shape mismatch: {g_shape} versus "
+                    f"{v_shape}"
+                )
+        self.values = values
+        self.grid_size = grid_size
+        self._set_indexers()
+
+    def _set_indexers(self):
+        n_polys = len(self.polygons)
+        n_lines = len(self.lines)
+        n_point = len(self.points)
+        if self.values is not None:
+            self.values, indexer = np.unique(self.values, return_inverse=True)
+            self.polygon_indexer, self.line_indexer, self.point_indexer = np.split(
+                indexer, [n_polys, n_polys + n_lines]
+            )
+        else:
+            self.polygon_indexer = np.arange(0, n_polys)
+            self.line_indexer = np.arange(n_polys, n_polys + n_lines)
+            self.point_indexer = np.arange(
+                n_polys + n_lines, n_polys + n_lines + n_point
+            )
+        return
+
+    def _copy_with(
+        self,
+        values=None,
+        polygons=None,
+        lines=None,
+        points=None,
+        polygon_indexer=None,
+        line_indexer=None,
+        point_indexer=None,
+    ) -> "Preprocessor":
+        """
+        Create a copy except for the provided keywords.
+
+        Used to return a new object easily to allow method chaining.
+        """
+        new_instance = object.__new__(self.__class__)
+        kwargs = {
+            "values": values,
+            "polygons": polygons,
+            "lines": lines,
+            "points": points,
+            "polygon_indexer": polygon_indexer,
+            "line_indexer": line_indexer,
+            "point_indexer": point_indexer,
+            "grid_size": None,
+        }
+        for k, v in kwargs.items():
+            if v is None:
+                v = getattr(self, k)
+            setattr(new_instance, k, v)
+        return new_instance
+
+    def to_geodataframe(self) -> gpd.GeoDataFrame:
+        """
+        Return the processed geometries as a ``geopandas.GeoDataFrame``.
+
+        Returns
+        -------
+        gdf: geopandas.GeoDataFrame
+            Contains columns geometry and indexer. The indexer column can be
+            used to (re-)associate the geometry with the original values.
+            If ``values`` were provided at initialization, a values column will
+            be added to the geodataframe.
+        """
+        geometry = np.concatenate(
+            (
+                self.polygons,
+                self.lines,
+                self.points,
+            )
+        )
+        indexer = np.concatenate(
+            (
+                self.polygon_indexer,
+                self.line_indexer,
+                self.point_indexer,
+            )
+        )
+        data = {"indexer": indexer}
+        if self.values is not None:
+            data["values"] = self.values[indexer]
+        return gpd.GeoDataFrame(
+            data=data,
+            geometry=geometry,
+        )
+
+    def merge_polygons(self) -> "Preprocessor":
+        """
+        Merge polygons with the same value for indexer (the same value if
+        ``values`` was provided at initialization).
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+        """
+        polygon_index = np.arange(len(self.polygons))
+        df = pd.DataFrame(
+            data={"polygon_index": polygon_index, "indexer": self.polygon_indexer}
+        )
+        merged_index = []
+        merged = []
+        for value, group in df.groupby("indexer"):
+            group_polygons = self.polygons[group["polygon_index"]]
+            merged_geometry = merge_polygons(group_polygons, grid_size=self.grid_size)
+            merged.extend(merged_geometry)
+            merged_index.extend([value] * len(merged_geometry))
+
+        return self._copy_with(polygons=merged, polygon_indexer=merged_index)
+
+    def unify_polygons(self, first: bool = True) -> "Preprocessor":
+        """
+        Resolve polygon overlaps and intersections.
+
+        In overview, this method takes the following steps:
+
+        1. collect all linear rings (exterior and interior boundaries), as well
+           as the linestrings.
+        2. create a unary union of all the linework. This ensures intersections
+           between lines are represented by a point on the lines.
+        3. polygonize the union linework. This creates a polygon for each ring that
+           is encountered, including holes.
+        4. collect sampling points for the newly created polygons. Use these to
+           locate in which polygon (or which hole!) the newly created polygon is
+           located.
+        5. In case of overlapping polygons, the sampling point may be present
+           in more than one of the original polygons. We choose the one with
+           the lowest indexer value or the smallest value in case ``.values``
+           was provided at initialization if ``first=True``; the highest
+           indexer value or largest value is taken for ``first=False``.
+        6. re-associate with the original indexer and discard hole polygons.
+
+        Unify polygons may generate many neighboring sub-polygons with the same
+        indexer value. The can be merged with ``.merge_polygons``.
+
+        Parameters
+        ----------
+        first: bool, optional
+            Which value or index to assign in case of polygon overlap.
+
+            In case ``values`` were provided at initialization:
+
+            * ``first=True``: take the smallest value among the overlapping
+              polygons.
+            * ``first=False``: take the largest value among the
+              overlapping polygons.
+
+            If not provided:
+
+            * ``first=True``: take the first overlapping polygon of the input
+              geometry.
+            * ``first=False``: take the last overlapping polygon of the input
+              geometry.
+
+            See the examples.
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+
+        Examples
+        --------
+        Resolve overlapping polygons, assigning the smallest cell size values
+        in case of overlap:
+
+        >>> processed = (
+        >>>     pandamesh.Preprocessor(geometry=gdf["geometry"], values=gdf["cellsize"])
+        >>>     .unify_polygons()
+        >>>     .merge_polygons(first=True)
+        >>>     .to_geodataframe()
+        >>> )
+
+        Assign the largest cell size value instead:
+
+        >>> processed = (
+        >>>     pandamesh.Preprocessor(geometry=gdf["geometry"], values=gdf["cellsize"])
+        >>>     .unify_polygons()
+        >>>     .merge_polygons(first=False)
+        >>>     .to_geodataframe()
+        >>> )
+
+        Alternatively, to control the result of the merging with values, we can
+        sort the ``gdf`` prior to processing.
+
+        >>> sorted_gdf = gdf.sort_values(["a", "b"])
+        >>> processor = pandamesh.Preprocessor(sorted_gdf["geometry"])
+
+        Afterwards, the returned indexer can be used to fetch the data associated
+        with the merged results.
+
+        >>> out = processor.to_geodataframe()
+        >>> processed = geopandas.GeoDataFrame(
+        >>>     data=sorted_gdf.iloc[out["indexer"]].loc[["a", "b"]],
+        >>>     geometry=out["geometry"],
+        >>> )
+        """
+        rings = np.concatenate(
+            (
+                collect_exteriors(self.polygons),
+                collect_interiors(self.polygons),
+                self.lines,
+            )
+        )
+        union = shapely.unary_union(rings, grid_size=self.grid_size)
+        polygonized_union = flatten_geometries(list(shapely.polygonize([union]).geoms))
+        new_polygons, new_indexer = locate_polygons(
+            new=polygonized_union,
+            old=self.polygons,
+            indexer=self.polygon_indexer,
+            ascending=first,
+        )
+        return self._copy_with(polygons=new_polygons, polygon_indexer=new_indexer)
+
+    def clip_lines(self, distance=0.0) -> "Preprocessor":
+        """
+        Remove line segments that are outside or that are near polygon
+        segments.
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+        """
+
+        if len(self.lines) == 0:
+            return self._copy_with()
+
+        # Discard line elements outside of the polygons.
+        all_polygons = shapely.unary_union(self.polygons, grid_size=self.grid_size)
+        lines_inside = flatten_geometries(
+            shapely.intersection(self.lines, all_polygons, grid_size=self.grid_size)
+        )
+
+        if distance > 0:
+            # Remove lines near to polygon boundaries.
+            rings = np.concatenate(
+                (
+                    collect_exteriors(self.polygons),
+                    collect_interiors(self.polygons),
+                )
+            )
+            exclusion = shapely.unary_union(
+                shapely.buffer(rings, distance=distance, cap_style="flat"),
+                grid_size=self.grid_size,
+            )
+            new_lines = shapely.difference(
+                lines_inside, exclusion, grid_size=self.grid_size
+            )
+            new_lines = new_lines[~shapely.is_empty(new_lines)]
+        else:
+            new_lines = lines_inside
+
+        new_indexer = locate_lines(new_lines, self.lines, self.line_indexer)
+        return self._copy_with(lines=new_lines, line_indexer=new_indexer)
+
+    def unify_lines(self) -> "Preprocessor":
+        """
+        Ensure intersections between lines are present.
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+        """
+        if len(self.lines) == 0:
+            return self._copy_with()
+
+        lines_union = np.asarray(
+            flatten_geometry(shapely.unary_union(self.lines, grid_size=self.grid_size))
+        )
+        lines_union = lines_union[shapely.get_type_id(lines_union) == 1]
+        # Sample halfway along the lines
+        sampling_points = shapely.line_interpolate_point(
+            lines_union, distance=0.5, normalized=True
+        )
+        tree = STRtree(self.lines)
+        index_original = tree.nearest(sampling_points)
+        return self._copy_with(
+            lines=lines_union, line_indexer=self.line_indexer[index_original]
+        )
+
+    def clip_points(self, distance: float = 0.0) -> "Preprocessor":
+        """
+        Remove points that are outside of a polygon or near line or
+        polygon segments.
+
+        Parameters
+        ----------
+        distance: float, optional
+            Minimum distance to a line or polygon segment.
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+        """
+        if len(self.points) == 0:
+            return self._copy_with()
+
+        # Check whether points are inside.
+        tree = STRtree(self.polygons)
+        inside, _ = tree.query(self.points, predicate="within")
+        new_points = self.points[inside]
+        new_indexer = self.point_indexer[inside]
+
+        # Check whether points aren't too near.
+        rings = np.concatenate(
+            (
+                collect_exteriors(self.polygons),
+                collect_interiors(self.polygons),
+                self.lines,
+            )
+        )
+        tree = STRtree(geoms=rings)
+        too_near, _ = tree.query(new_points, predicate="dwithin", distance=distance)
+        keep = np.full(len(new_points), True)
+        keep[too_near] = False
+        return self._copy_with(points=new_points[keep], point_indexer=new_indexer[keep])
+
+    def interpolate_lines_to_points(
+        self, distance: Optional[float] = None, values_as_distance: bool = False
+    ) -> "Preprocessor":
+        """
+        Convert lines into points.
+
+        This method adds vertices if needed, but it does not discard vertices
+        that are located close together: ``distance`` is the maximum distance,
+        not a minimum.
+
+        Parameters
+        ----------
+        distance: float, optional
+            Additional vertices will be added so that all line segments are no
+            longer than this value. Must be greater than 0.
+        values_as_distance: bool, optional
+            If true, ignores the value of ``distance`` and uses ``values``
+            provided during initialization as the distance instead. Errors if
+            no values have been provided.
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+        """
+        if len(self.lines) == 0:
+            return self._copy_with()
+
+        if values_as_distance:
+            distance = self.values[self.line_indexer]
+        elif distance is None:
+            raise ValueError(
+                "If values_as_distance is False, distance must be provided."
+            )
+
+        segmentized = shapely.segmentize(self.lines, distance)
+        new_points_xy, index = shapely.get_coordinates(segmentized, return_index=True)
+        return self._copy_with(
+            points=np.concatenate((shapely.points(new_points_xy), self.points)),
+            point_indexer=np.concatenate(
+                (self.point_indexer, self.line_indexer[index])
+            ),
+            lines=np.empty(0, dtype=object),
+            line_indexer=np.empty(0, dtype=int),
+        )
+
+    def snap_points(self, distance: float) -> "Preprocessor":
+        """
+        Snap points together that are within tolerance of each other.
+
+        Will use Numba to accelerate the snapping if it is installed. This may
+        be significantly faster in case of snapping a large (>10 000) number of points.
+
+        Returns
+        -------
+        processed: pandamesh.Preprocessor
+        """
+        if len(self.points) == 0:
+            return self._copy_with()
+        index = snap_nodes(shapely.get_coordinates(self.points), distance)
+        return self._copy_with(
+            points=self.points[index],
+            point_indexer=self.point_indexer[index],
+        )
diff --git a/pandamesh/snapping.py b/pandamesh/snapping.py
new file mode 100644
index 0000000..2a8d801
--- /dev/null
+++ b/pandamesh/snapping.py
@@ -0,0 +1,159 @@
+from typing import NamedTuple
+
+import numpy as np
+from scipy import sparse
+from scipy.spatial import KDTree
+
+from pandamesh.common import FloatArray, IntArray
+
+try:
+    from numba import njit  # pragma: no cover
+except ImportError:
+
+    def njit(*args, **kwargs):  # pragma: no cover
+        # Dummy decorator when numba is not available.
+        def decorator(func):
+            return func
+
+        return decorator
+
+
+class MatrixCSR(NamedTuple):
+    """
+    Compressed Sparse Row matrix. The row indices are compressed; all values
+    must therefore be sorted by row number. More or less matches the
+    scipy.sparse.csr_matrix.
+
+    NamedTuple for easy ingestion by numba.
+
+    Parameters
+    ----------
+    data: np.ndarray of floats
+        The values of the matrix.
+    indices: np.ndarray of integers
+        The column numbers of the CSR format.
+    indptr: inp.ndarray of integers
+        The row index CSR pointer array.
+        Values for row i (target index i) are stored in:
+        indices[indptr[i]: indptr[i + 1]]
+    n: int
+        The number of rows.
+    m: int
+        The number of columns.
+    nnz: int
+        The number of non-zero values.
+    """
+
+    data: FloatArray
+    indices: IntArray
+    indptr: IntArray
+    n: int
+    m: int
+    nnz: int
+
+    @staticmethod
+    def from_csr_matrix(A: sparse.csr_matrix) -> "MatrixCSR":
+        n, m = A.shape
+        return MatrixCSR(A.data, A.indices, A.indptr, n, m, A.nnz)
+
+
+@njit(inline="always")
+def row_slice(A, row: int) -> slice:
+    """Return the indices or data slice of a single row."""
+    start = A.indptr[row]
+    end = A.indptr[row + 1]
+    return slice(start, end)
+
+
+@njit(inline="always")
+def columns_and_values(A, slice):
+    return zip(A.indices[slice], A.data[slice])
+
+
+@njit(cache=True)
+def _snap_to_nearest(A: MatrixCSR, snap_candidates: IntArray, max_distance) -> IntArray:
+    """
+    Find a closest target for each node.
+
+    The kD tree distance matrix will have stored for each node the other nodes
+    that are within snapping distance. These are the rows in the sparse matrix
+    that have more than one entry: the snap_candidates.
+
+    The first condition for a point to become a TARGET is if it hasn't been
+    connected to another point yet, i.e. it is UNVISITED. Once a point becomes
+    an TARGET, it looks for nearby points within the max_distance. These nearby
+    points are connected if: they are UNVISITED (i.e. they don't have a target
+    yet), or the current target is closer than the previous.
+    """
+    UNVISITED = -1
+    TARGET = -2
+    nearest = np.full(A.n, max_distance + 1.0)
+    visited = np.full(A.n, UNVISITED)
+
+    for i in snap_candidates:
+        if visited[i] != UNVISITED:
+            continue
+        visited[i] = TARGET
+
+        # Now iterate through every node j that is within max_distance of node i.
+        for j, dist in columns_and_values(A, row_slice(A, i)):
+            if i == j or visited[j] == TARGET:
+                # Continue if we're looking at the distance to ourselves
+                # (i==j), or other node is a target.
+                continue
+            if visited[j] == UNVISITED or dist < nearest[j]:
+                # If unvisited node, or already visited but we're closer, set
+                # to i.
+                visited[j] = i
+                nearest[j] = dist
+
+    return visited
+
+
+def snap_nodes(xy: FloatArray, max_snap_distance: float) -> IntArray:
+    """
+    Snap neigbhoring vertices together that are located within a maximum
+    snapping distance from each other.
+
+    If vertices are located within a maximum distance, some of them are snapped
+    to their neighbors ("targets"), thereby guaranteeing a minimum distance
+    between nodes in the result. The determination of whether a point becomes a
+    target itself or gets snapped to another point is primarily based on the
+    order in which points are processed and their spatial relationships.
+
+    This function also return an inverse index array. In case of a connectivity
+    array, ``inverse`` can be used to index into, yielding the updated
+    numbers. E.g.:
+
+    ``updated_face_nodes = inverse[face_nodes]``
+
+    Parameters
+    ----------
+    xy: nd array of floats of size (N, 2)
+    max_snap_distance: float
+
+    Returns
+    -------
+    inverse: 1D nd array of ints of size N
+        Inverse index array: the new vertex number for every old vertex. Is
+    """
+    # First, find all the points that lie within max_distance of each other
+    tree = KDTree(xy)
+    distances = tree.sparse_distance_matrix(
+        tree, max_distance=max_snap_distance, output_type="coo_matrix"
+    ).tocsr()
+    should_snap = distances.getnnz(axis=1) > 1
+
+    if should_snap.any():
+        index = np.arange(len(xy))
+        visited = _snap_to_nearest(
+            A=MatrixCSR.from_csr_matrix(distances),
+            snap_candidates=index[should_snap],
+            max_distance=max_snap_distance,
+        )
+        targets = visited < 0  # i.e. still UNVISITED or TARGET valued.
+        visited[targets] = index[targets]
+        deduplicated = np.unique(visited)
+        return deduplicated
+    else:
+        return np.arange(len(xy))
diff --git a/pandamesh/triangle_enums.py b/pandamesh/triangle_enums.py
new file mode 100644
index 0000000..3e79089
--- /dev/null
+++ b/pandamesh/triangle_enums.py
@@ -0,0 +1,18 @@
+from pandamesh.enum_base import FlexibleEnum
+
+
+class DelaunayAlgorithm(FlexibleEnum):
+    """The type of Delaunay algorithm for Triangle."""
+
+    DIVIDE_AND_CONQUER = ""
+    """Default algorithm."""
+    INCREMENTAL = "i"
+    """
+    Uses the incremental algorithm for Delaunay triangulation, rather than
+    the divide-and-conquer algorithm.
+    """
+    SWEEPLINE = "F"
+    """
+    Uses Steven Fortune's sweepline algorithm for Delaunay triangulation,
+    rather than the divide-and-conquer algorithm.
+    """
diff --git a/pandamesh/triangle_geometry.py b/pandamesh/triangle_geometry.py
index 6812d65..4700fb2 100644
--- a/pandamesh/triangle_geometry.py
+++ b/pandamesh/triangle_geometry.py
@@ -4,9 +4,8 @@
 import numpy as np
 import pandas as pd
 import shapely
-import shapely.geometry as sg
 
-from pandamesh.common import FloatArray, IntArray, flatten
+from pandamesh.common import FloatArray, Grouper, IntArray, flatten, flatten_geometry
 
 
 def segmentize_linestrings(linestrings: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
@@ -49,7 +48,8 @@ def add_polygons(
     regions[:, 1] = region_points.y
     regions[:, 2] = np.arange(n_region)
     cellsize = polygons[is_region]["cellsize"].to_numpy()
-    regions[:, 3] = 0.5 * cellsize * cellsize
+    # Assume equilateral triangles for cell size to area conversion.
+    regions[:, 3] = 0.25 * np.sqrt(3) * cellsize * cellsize
 
     boundary = polygons.boundary.explode(index_parts=True).geometry
     vertices, segments = add_linestrings(boundary)
@@ -72,18 +72,34 @@ def polygon_holes(
 
     Triangle recognizes holes as a point contained by segments.
     """
-    inner_rings = gpd.GeoSeries(flatten(polygons.interiors))
-    interiors = gpd.GeoDataFrame(geometry=[sg.Polygon(ring) for ring in inner_rings])
-    points = interiors.representative_point()
-    # Filter the points, if the point can be found in a polygon, it's located
-    # in a refinement zone.
-    points_inside = polygons.sjoin(
-        gpd.GeoDataFrame(geometry=points), predicate="contains"
+    # An interior may be a true hole, or it could be (partially!) filled with
+    # another polygon. Find out if this is the case: get the interiors, and
+    # diff them with any polygon inside.
+    inner_rings = flatten(polygons.interiors)
+    interiors = np.asarray(flatten_geometry(shapely.polygonize(inner_rings)))
+    tree = shapely.STRtree(interiors)
+    index_inside, index_interior = tree.query(
+        polygons.representative_point(), predicate="within"
     )
-    keep = np.full(len(points), True)
-    keep[points_inside["index_right"].to_numpy().astype(int)] = False
-    points = points[keep]
-
+    nothing_inside = np.full(len(interiors), True)
+    nothing_inside[index_interior] = False
+
+    points = [gpd.GeoSeries(interiors[nothing_inside]).representative_point()]
+    if len(index_inside) > 0:
+        for interior, polygons_inside in Grouper(
+            a=interiors,
+            a_index=index_interior,
+            b=polygons.geometry,
+            b_index=index_inside,
+        ):
+            all_polygons = shapely.unary_union(polygons_inside)
+            true_holes = shapely.difference(interior, all_polygons)
+            if shapely.is_empty(true_holes).all():
+                continue
+            hole_points = gpd.GeoSeries(true_holes).explode().representative_point()
+            points.append(hole_points)
+
+    points = gpd.GeoSeries(np.concatenate(points))
     if len(points) > 0:
         return add_points(points)
     else:
diff --git a/pandamesh/triangle_mesher.py b/pandamesh/triangle_mesher.py
index 148ce44..bc1d0e6 100644
--- a/pandamesh/triangle_mesher.py
+++ b/pandamesh/triangle_mesher.py
@@ -1,4 +1,3 @@
-from enum import Enum
 from typing import Tuple, Union
 
 import geopandas as gpd
@@ -8,28 +7,22 @@
     FloatArray,
     IntArray,
     check_geodataframe,
-    invalid_option,
     repr,
     separate,
-    to_ugrid,
 )
+from pandamesh.mesher_base import MesherBase
+from pandamesh.triangle_enums import DelaunayAlgorithm
 from pandamesh.triangle_geometry import collect_geometry, polygon_holes
 
 
-class DelaunayAlgorithm(Enum):
-    DIVIDE_AND_CONQUER = ""
-    INCREMENTAL = "i"
-    SWEEPLINE = "F"
-
-
-class TriangleMesher:
+class TriangleMesher(MesherBase):
     """
     Wrapper for the python bindings to Triangle. This class must be initialized
     with a geopandas GeoDataFrame containing at least one polygon, and a column
     named ``"cellsize"``.
 
     Optionally, multiple polygons with different cell sizes can be included in
-    the geodataframe. These can be used to achieve local mesh remfinement.
+    the geodataframe. These can be used to achieve local mesh refinement.
 
     Linestrings and points may also be included. The segments of linestrings
     will be directly forced into the triangulation. Points can also be forced
@@ -41,7 +34,8 @@ class TriangleMesher:
     the geodataframe are checked:
 
         * Polygons should not have any overlap with each other.
-        * Linestrings should not intersect each other.
+        * Linestrings should not intersect each other, unless the intersection
+          vertex is present in both.
         * Every linestring should be fully contained by a single polygon;
           a linestring may not intersect two or more polygons.
         * Linestrings and points should not "touch" / be located on
@@ -49,7 +43,8 @@ class TriangleMesher:
         * Holes in polygons are fully supported, but they must not contain
           any linestrings or points.
 
-    If such cases are detected, the initialization will error.
+    If such cases are detected, the initialization will error: use the
+    :class:`pandamesh.Preprocessor` to clean up geometries beforehand.
 
     For more details on Triangle, see:
     https://www.cs.cmu.edu/~quake/triangle.defs.html
@@ -142,21 +137,21 @@ def maximum_steiner_points(self, value: Union[int, None]):
     @property
     def delaunay_algorithm(self) -> DelaunayAlgorithm:
         """
-        ``DelaunayAlgoritm.DIVIDE_AND_CONQUER``: Default algorithm.
+        Sets the Delaunay algorithm. Can be set to one of:
+        :py:class:`pandamesh.DelaunayAlgorithm`:
 
-        ``DelaunayAlgoritm.INCREMENTAL``: Uses the incremental algorithm for
-        Delaunay triangulation, rather than the divide-and-conquer algorithm.
+        .. code::
+
+            DIVIDE_AND_CONQUER = ""
+            INCREMENTAL = "i"
+            SWEEPLINE = "F"
 
-        ``DelaunayAlgoritm.SWEEPLINE``: Uses Steven Fortune’s sweepline
-        algorithm for Delaunay triangulation, rather than the
-        divide-and-conquer algorithm.
         """
         return self._delaunay_algorithm
 
     @delaunay_algorithm.setter
-    def delaunay_algorithm(self, value: DelaunayAlgorithm):
-        if value not in DelaunayAlgorithm:
-            raise ValueError(invalid_option(value, DelaunayAlgorithm))
+    def delaunay_algorithm(self, value: Union[DelaunayAlgorithm, str]):
+        value = DelaunayAlgorithm.from_value(value)
         self._delaunay_algorithm = value
 
     @property
@@ -202,6 +197,3 @@ def generate(self) -> Tuple[FloatArray, IntArray]:
 
         result = triangle.triangulate(tri=tri, opts=options)
         return result["vertices"], result["triangles"]
-
-    def generate_ugrid(self) -> "xugrid.Ugrid2d":  # type: ignore # noqa
-        return to_ugrid(*self.generate())
diff --git a/pixi.lock b/pixi.lock
index ab6570b..691c966 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -19,19 +19,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h8a0f778_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.2-h7970872_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.25-h4bc722e_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h04a40c0_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-hc9bb02b_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h3e50d33_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h674cf7e_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-hbe604ca_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-hc649ecc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.5-hba11562_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-he20dfa5_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h15d0e8c_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.3-h8dac057_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.27-h4bc722e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-haa50ccc_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h570d160_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-h1c59cda_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-hf5b9b93_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hc14a930_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-h558cea2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h038f3f9_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h038f3f9_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.6-h1966bd9_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-hf9693f6_5.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda
@@ -334,7 +334,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py312hf008fa9_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.0-h3400bea_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.1-h3400bea_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.5.1-py312h775a589_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.14.0-py312hc2bc53b_1.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py312h7900ff3_2.conda
@@ -359,7 +359,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h213c483_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h33a2d6f_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -429,19 +429,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h6309471_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.2-h5b118bc_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.25-hfdf4475_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.2-h671840a_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h22e442e_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hddd658a_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h3710d3a_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-h89cdba3_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h5b118bc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.5-h53ac995_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-h065e723_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h4880c77_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.3-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.27-hfdf4475_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h3e75f19_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-h324d61a_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h3967ab9_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf47059d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h93740dc_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-hd06a241_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-hf37c103_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.6-hb7fc533_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-hdc2ca5d_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.13.0-hf8dbe3c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.8.0-h60298e3_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.12.0-h646f05d_0.conda
@@ -745,7 +745,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h6b8956e_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h68e3656_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -805,19 +805,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-h08a4710_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.2-h98f14e4_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.25-h99b78c6_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.2-hf1484eb_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-h624841c_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hacc3fab_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h8469d54_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-ha2a438e_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h98f14e4_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.5-h7061aca_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h0db37eb_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-hd1432f0_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.3-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.27-h99b78c6_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41e72e7_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h79ff00d_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-ha3a5f06_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-h43341e6_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h64da278_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-h3f26dbc_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h85401af_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.6-h3fdcf82_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h34e68ec_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.13.0-hd01fc5c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.8.0-h13ea094_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.12.0-hfde595f_0.conda
@@ -1121,7 +1121,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h05a3d2a_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h183c882_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -1180,19 +1180,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-he6981b3_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.2-h0240d57_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.25-h2466b09_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.2-h330dd76_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h065a16f_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3095dc9_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h1efefa3_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h9116173_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-h0240d57_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.5-h55e8508_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-h2205d66_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-h7db803d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.3-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.27-h2466b09_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-ha1e9ad3_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hf2a634e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h7d4ba70_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h8d4122e_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h827f298_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h21392f2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-ha1e9ad3_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.6-h6fef074_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-hd66f94a_5.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.13.0-haf5610f_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.8.0-h148e6f0_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.12.0-hf03c1c4_0.conda
@@ -1498,7 +1498,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-hc790b64_3.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-h3c7d8a4_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-hc6d05bc_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -1571,19 +1571,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h8a0f778_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.2-h7970872_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.25-h4bc722e_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h04a40c0_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-hc9bb02b_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h3e50d33_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h674cf7e_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-hbe604ca_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-hc649ecc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.5-hba11562_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-he20dfa5_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h15d0e8c_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.3-h8dac057_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.27-h4bc722e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-haa50ccc_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h570d160_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-h1c59cda_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-hf5b9b93_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hc14a930_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-h558cea2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h038f3f9_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h038f3f9_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.6-h1966bd9_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-hf9693f6_5.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda
@@ -1887,7 +1887,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py39h5cde264_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.0-h3400bea_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.1-h3400bea_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.5.1-py39hf7b0125_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.13.1-py39haf93ffa_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py39hf3d152e_2.conda
@@ -1912,7 +1912,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h213c483_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h33a2d6f_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -1983,19 +1983,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h6309471_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.2-h5b118bc_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.25-hfdf4475_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.2-h671840a_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h22e442e_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hddd658a_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h3710d3a_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-h89cdba3_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h5b118bc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.5-h53ac995_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-h065e723_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h4880c77_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.3-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.27-hfdf4475_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h3e75f19_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-h324d61a_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h3967ab9_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf47059d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h93740dc_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-hd06a241_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-hf37c103_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.6-hb7fc533_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-hdc2ca5d_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.13.0-hf8dbe3c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.8.0-h60298e3_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.12.0-h646f05d_0.conda
@@ -2300,7 +2300,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h6b8956e_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h68e3656_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -2361,19 +2361,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-h08a4710_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.2-h98f14e4_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.25-h99b78c6_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.2-hf1484eb_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-h624841c_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hacc3fab_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h8469d54_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-ha2a438e_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h98f14e4_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.5-h7061aca_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h0db37eb_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-hd1432f0_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.3-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.27-h99b78c6_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41e72e7_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h79ff00d_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-ha3a5f06_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-h43341e6_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h64da278_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-h3f26dbc_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h85401af_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.6-h3fdcf82_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h34e68ec_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.13.0-hd01fc5c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.8.0-h13ea094_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.12.0-hfde595f_0.conda
@@ -2678,7 +2678,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h05a3d2a_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h183c882_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -2738,19 +2738,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-he6981b3_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.2-h0240d57_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.25-h2466b09_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.2-h330dd76_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h065a16f_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3095dc9_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h1efefa3_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h9116173_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-h0240d57_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.5-h55e8508_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-h2205d66_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-h7db803d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.3-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.27-h2466b09_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-ha1e9ad3_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hf2a634e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h7d4ba70_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h8d4122e_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h827f298_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h21392f2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-ha1e9ad3_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.6-h6fef074_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-hd66f94a_5.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.13.0-haf5610f_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.8.0-h148e6f0_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.12.0-hf03c1c4_0.conda
@@ -3057,7 +3057,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-hc790b64_3.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-h3c7d8a4_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-hc6d05bc_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -3131,19 +3131,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h8a0f778_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.2-h7970872_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.25-h4bc722e_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h04a40c0_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-hc9bb02b_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h3e50d33_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h674cf7e_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-hbe604ca_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-hc649ecc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.5-hba11562_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-he20dfa5_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h15d0e8c_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.3-h8dac057_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.27-h4bc722e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-haa50ccc_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h570d160_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-h1c59cda_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-hf5b9b93_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hc14a930_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-h558cea2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h038f3f9_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h038f3f9_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.6-h1966bd9_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-hf9693f6_5.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda
@@ -3446,7 +3446,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py310h42e942d_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.0-h3400bea_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.1-h3400bea_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.5.1-py310h146d792_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.14.0-py310h93e2701_1.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py310hff52083_2.conda
@@ -3471,7 +3471,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h213c483_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h33a2d6f_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -3542,19 +3542,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h6309471_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.2-h5b118bc_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.25-hfdf4475_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.2-h671840a_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h22e442e_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hddd658a_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h3710d3a_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-h89cdba3_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h5b118bc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.5-h53ac995_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-h065e723_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h4880c77_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.3-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.27-hfdf4475_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h3e75f19_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-h324d61a_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h3967ab9_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf47059d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h93740dc_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-hd06a241_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-hf37c103_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.6-hb7fc533_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-hdc2ca5d_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.13.0-hf8dbe3c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.8.0-h60298e3_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.12.0-h646f05d_0.conda
@@ -3858,7 +3858,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h6b8956e_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h68e3656_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -3919,19 +3919,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-h08a4710_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.2-h98f14e4_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.25-h99b78c6_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.2-hf1484eb_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-h624841c_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hacc3fab_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h8469d54_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-ha2a438e_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h98f14e4_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.5-h7061aca_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h0db37eb_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-hd1432f0_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.3-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.27-h99b78c6_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41e72e7_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h79ff00d_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-ha3a5f06_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-h43341e6_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h64da278_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-h3f26dbc_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h85401af_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.6-h3fdcf82_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h34e68ec_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.13.0-hd01fc5c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.8.0-h13ea094_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.12.0-hfde595f_0.conda
@@ -4235,7 +4235,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h05a3d2a_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h183c882_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -4295,19 +4295,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-he6981b3_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.2-h0240d57_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.25-h2466b09_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.2-h330dd76_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h065a16f_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3095dc9_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h1efefa3_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h9116173_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-h0240d57_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.5-h55e8508_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-h2205d66_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-h7db803d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.3-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.27-h2466b09_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-ha1e9ad3_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hf2a634e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h7d4ba70_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h8d4122e_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h827f298_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h21392f2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-ha1e9ad3_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.6-h6fef074_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-hd66f94a_5.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.13.0-haf5610f_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.8.0-h148e6f0_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.12.0-hf03c1c4_0.conda
@@ -4613,7 +4613,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-hc790b64_3.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-h3c7d8a4_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-hc6d05bc_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -4687,19 +4687,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h8a0f778_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.2-h7970872_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.25-h4bc722e_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h04a40c0_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-hc9bb02b_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h3e50d33_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h674cf7e_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-hbe604ca_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-hc649ecc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.5-hba11562_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-he20dfa5_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h15d0e8c_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.3-h8dac057_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.27-h4bc722e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-haa50ccc_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h570d160_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-h1c59cda_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-hf5b9b93_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hc14a930_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-h558cea2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h038f3f9_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h038f3f9_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.6-h1966bd9_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-hf9693f6_5.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda
@@ -5002,7 +5002,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py311hb3a8bbb_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.0-h3400bea_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.1-h3400bea_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.5.1-py311hd632256_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.14.0-py311h517d4fd_1.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py311h38be061_2.conda
@@ -5027,7 +5027,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h213c483_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h33a2d6f_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -5097,19 +5097,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h6309471_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.2-h5b118bc_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.25-hfdf4475_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.2-h671840a_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h22e442e_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hddd658a_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h3710d3a_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-h89cdba3_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h5b118bc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.5-h53ac995_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-h065e723_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h4880c77_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.3-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.27-hfdf4475_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h3e75f19_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-h324d61a_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h3967ab9_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf47059d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h93740dc_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-hd06a241_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-hf37c103_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.6-hb7fc533_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-hdc2ca5d_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.13.0-hf8dbe3c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.8.0-h60298e3_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.12.0-h646f05d_0.conda
@@ -5413,7 +5413,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h6b8956e_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h68e3656_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -5473,19 +5473,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-h08a4710_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.2-h98f14e4_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.25-h99b78c6_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.2-hf1484eb_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-h624841c_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hacc3fab_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h8469d54_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-ha2a438e_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h98f14e4_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.5-h7061aca_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h0db37eb_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-hd1432f0_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.3-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.27-h99b78c6_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41e72e7_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h79ff00d_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-ha3a5f06_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-h43341e6_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h64da278_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-h3f26dbc_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h85401af_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.6-h3fdcf82_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h34e68ec_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.13.0-hd01fc5c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.8.0-h13ea094_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.12.0-hfde595f_0.conda
@@ -5789,7 +5789,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h05a3d2a_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h183c882_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -5848,19 +5848,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-he6981b3_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.2-h0240d57_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.25-h2466b09_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.2-h330dd76_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h065a16f_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3095dc9_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h1efefa3_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h9116173_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-h0240d57_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.5-h55e8508_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-h2205d66_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-h7db803d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.3-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.27-h2466b09_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-ha1e9ad3_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hf2a634e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h7d4ba70_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h8d4122e_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h827f298_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h21392f2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-ha1e9ad3_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.6-h6fef074_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-hd66f94a_5.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.13.0-haf5610f_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.8.0-h148e6f0_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.12.0-hf03c1c4_0.conda
@@ -6166,7 +6166,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-hc790b64_3.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-h3c7d8a4_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-hc6d05bc_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -6239,19 +6239,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h8a0f778_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.2-h7970872_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.25-h4bc722e_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h04a40c0_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-hc9bb02b_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h3e50d33_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h674cf7e_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-hbe604ca_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-hc649ecc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-hc649ecc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.5-hba11562_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-he20dfa5_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h15d0e8c_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.3-h8dac057_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.27-h4bc722e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-haa50ccc_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h570d160_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-h1c59cda_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-hf5b9b93_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hc14a930_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-h558cea2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h038f3f9_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h038f3f9_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.6-h1966bd9_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-hf9693f6_5.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda
@@ -6554,7 +6554,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2
       - conda: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.20.0-py312hf008fa9_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.0-h3400bea_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.1-h3400bea_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.5.1-py312h775a589_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.14.0-py312hc2bc53b_1.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/secretstorage-3.3.3-py312h7900ff3_2.conda
@@ -6579,7 +6579,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh0d859eb_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h213c483_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h33a2d6f_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -6649,19 +6649,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h6309471_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.2-h5b118bc_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.25-hfdf4475_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.2-h671840a_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h22e442e_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hddd658a_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h3710d3a_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-h89cdba3_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h5b118bc_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-h5b118bc_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.5-h53ac995_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-h065e723_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h4880c77_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.3-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.27-hfdf4475_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h3e75f19_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-h324d61a_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h3967ab9_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf47059d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h93740dc_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-hd06a241_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-hf37c103_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-hf37c103_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.6-hb7fc533_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-hdc2ca5d_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-core-cpp-1.13.0-hf8dbe3c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-identity-cpp-1.8.0-h60298e3_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/azure-storage-blobs-cpp-12.12.0-h646f05d_0.conda
@@ -6965,7 +6965,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h6b8956e_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h68e3656_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-64/tk-8.6.13-h1abcd95_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -7025,19 +7025,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-h08a4710_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.2-h98f14e4_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.25-h99b78c6_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.2-hf1484eb_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-h624841c_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hacc3fab_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h8469d54_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-ha2a438e_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h98f14e4_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h98f14e4_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.5-h7061aca_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h0db37eb_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-hd1432f0_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.3-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.27-h99b78c6_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41e72e7_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h79ff00d_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-ha3a5f06_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-h43341e6_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h64da278_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-h3f26dbc_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h85401af_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h85401af_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.6-h3fdcf82_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h34e68ec_5.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-core-cpp-1.13.0-hd01fc5c_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-identity-cpp-1.8.0-h13ea094_2.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/azure-storage-blobs-cpp-12.12.0-hfde595f_0.conda
@@ -7341,7 +7341,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh31c8845_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h05a3d2a_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h183c882_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h5083fa2_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -7400,19 +7400,19 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-24.2.0-pyh71513ae_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-he6981b3_4.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.2-h0240d57_1.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.25-h2466b09_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.2-h330dd76_20.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h065a16f_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3095dc9_2.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h1efefa3_16.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h9116173_6.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-h0240d57_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-h0240d57_8.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.5-h55e8508_5.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-h2205d66_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-h7db803d_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.3-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.27-h2466b09_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-ha1e9ad3_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hf2a634e_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h7d4ba70_5.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h8d4122e_6.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h827f298_17.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h21392f2_8.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-ha1e9ad3_2.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-ha1e9ad3_10.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.6-h6fef074_0.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-hd66f94a_5.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-core-cpp-1.13.0-haf5610f_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-identity-cpp-1.8.0-h148e6f0_2.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/azure-storage-blobs-cpp-12.12.0-hf03c1c4_0.conda
@@ -7718,7 +7718,7 @@ environments:
       - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.12.0-hc790b64_3.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh5737063_0.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.5.0-pyhc1e730c_0.conda
-      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-h3c7d8a4_7.conda
+      - conda: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-hc6d05bc_8.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.3.0-pyhd8ed1ab_0.conda
       - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda
       - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2
@@ -8332,173 +8332,173 @@ packages:
 - kind: conda
   name: aws-c-auth
   version: 0.7.25
-  build: h08a4710_4
-  build_number: 4
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-h08a4710_4.conda
-  sha256: aff4fc098e6e38f4e3b50f00070e7fb4298148e3811c03b398db2415089eed19
-  md5: 8057986b77e1776a05139d046905a1e1
+  build: h15d0e8c_6
+  build_number: 6
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h15d0e8c_6.conda
+  sha256: 0680ca18238e17d319f87bb8390d116292592c6f5534c66404542665d6149fae
+  md5: e0d292ba383ac09598c664186c0144cd
   depends:
-  - __osx >=11.0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
+  - libgcc-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 92521
-  timestamp: 1723215432657
+  size: 107286
+  timestamp: 1723725324766
 - kind: conda
   name: aws-c-auth
   version: 0.7.25
-  build: h6309471_4
-  build_number: 4
+  build: h4880c77_6
+  build_number: 6
   subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h6309471_4.conda
-  sha256: 1bc76a69f0342dde6943f4aeaf410d7ce97fd59e7fd01a3b6493265d46417535
-  md5: d5e750ceeb6b8e7a4091b72fc35fafcf
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-auth-0.7.25-h4880c77_6.conda
+  sha256: 31085909daaea5bc3d8288577a4f63de57a4bd7ab6510d0946c03b70edc1745d
+  md5: 31c5fb2e092df17d88d315f1deca5af6
   depends:
   - __osx >=10.13
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 93886
-  timestamp: 1723215237016
+  size: 94011
+  timestamp: 1723725415937
 - kind: conda
   name: aws-c-auth
   version: 0.7.25
-  build: h8a0f778_4
-  build_number: 4
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.25-h8a0f778_4.conda
-  sha256: 06f0fd8fdcee90c45d3f711dae3f7439f5895e3ef0c498484a2a67c781eff748
-  md5: aa51e240a99df535e64e3472f54652c9
+  build: h7db803d_6
+  build_number: 6
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-h7db803d_6.conda
+  sha256: 9dcb467b8f6b33c44fb4644501bcfe8dd0df9cd066b27e725aae22688a3fda01
+  md5: c17d2724a6f73adcaaf39d1271ca20b1
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
-  - libgcc-ng >=12
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 106691
-  timestamp: 1723215154132
+  size: 102584
+  timestamp: 1723725722832
 - kind: conda
   name: aws-c-auth
   version: 0.7.25
-  build: he6981b3_4
-  build_number: 4
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-auth-0.7.25-he6981b3_4.conda
-  sha256: 1d6c1e645ca6dbfe240b038cf69612b920af939db638d6c76672d1d709eb2522
-  md5: 24b3c39664741663f52423700f8a9d39
+  build: hd1432f0_6
+  build_number: 6
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-auth-0.7.25-hd1432f0_6.conda
+  sha256: 312444985a6c09849cc81c36a4187720efc4f39258e2ec8cafc595a5b252d3c1
+  md5: eb4e83993ec5fb4eebcfaf931d8217dd
   depends:
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=11.0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 102694
-  timestamp: 1723215631851
+  size: 92129
+  timestamp: 1723725599539
 - kind: conda
   name: aws-c-cal
-  version: 0.7.2
-  build: h0240d57_1
-  build_number: 1
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.2-h0240d57_1.conda
-  sha256: 2af1e06136c0e70e420c33ffcfa3d0c750612b52d63c1a2312e8d700286b937d
-  md5: c5efc4764f0ef900773ce65ce22b1d30
+  version: 0.7.3
+  build: h85401af_2
+  build_number: 2
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.3-h85401af_2.conda
+  sha256: 9b8fc6484fea032552ac61f2a45f073cbb28d7bc37f2166e27c8e0247ac857a2
+  md5: 4adcf0d38e19822d4597d592fb746912
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=11.0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - openssl >=3.3.1,<4.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 46834
-  timestamp: 1723150669417
+  size: 39962
+  timestamp: 1723674631762
 - kind: conda
   name: aws-c-cal
-  version: 0.7.2
-  build: h5b118bc_1
-  build_number: 1
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.2-h5b118bc_1.conda
-  sha256: c25ac0b41c728d8cab471517b97f65388b82f800123b275555605b37aea2e16a
-  md5: 0ece7b2462d35fc9de1bc2015e42a243
+  version: 0.7.3
+  build: h8dac057_2
+  build_number: 2
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.3-h8dac057_2.conda
+  sha256: bfd4f73855e926e6c7c9db700a17ef3ddb0e848b85edd04d766d50a008835407
+  md5: 577509458a061ddc9b089602ac6e1e98
   depends:
-  - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - libgcc-ng >=12
   - openssl >=3.3.1,<4.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 39014
-  timestamp: 1723150485190
+  size: 47317
+  timestamp: 1723674520890
 - kind: conda
   name: aws-c-cal
-  version: 0.7.2
-  build: h7970872_1
-  build_number: 1
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.2-h7970872_1.conda
-  sha256: 060d9fba6b18781aff862ca84b5b11f4681f6dc364c70856e5ab5d61cf5eb324
-  md5: f7f648ab6e51b66d2ada922498547732
+  version: 0.7.3
+  build: ha1e9ad3_2
+  build_number: 2
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-cal-0.7.3-ha1e9ad3_2.conda
+  sha256: 1bb4570afeb28f6b4693b4cb037d823308b40020b779f42d508d42dab0b44eaa
+  md5: c763ff9c4c712558ef72a460e4a2dbae
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - libgcc-ng >=12
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - openssl >=3.3.1,<4.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 47118
-  timestamp: 1723150410207
+  size: 47054
+  timestamp: 1723675012781
 - kind: conda
   name: aws-c-cal
-  version: 0.7.2
-  build: h98f14e4_1
-  build_number: 1
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-cal-0.7.2-h98f14e4_1.conda
-  sha256: 62e0b3e566eb9b6b33c4a64a1bf9424395d1941668881edbe71f325563e322a4
-  md5: 50c3bc26750929036f201a469968ee55
+  version: 0.7.3
+  build: hf37c103_2
+  build_number: 2
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-cal-0.7.3-hf37c103_2.conda
+  sha256: b008a16ac17edb0f5ecae88ee512ca6a1d5fdee3e4068eb73bd9656e7844ee3c
+  md5: 741ce62d05d8f62a74644c75f0179615
   depends:
-  - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=10.13
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - openssl >=3.3.1,<4.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 39925
-  timestamp: 1723150644067
+  size: 39391
+  timestamp: 1723674565375
 - kind: conda
   name: aws-c-common
-  version: 0.9.25
+  version: 0.9.27
   build: h2466b09_0
   subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.25-h2466b09_0.conda
-  sha256: ce2a6a0c430bb70f250ac58ca65fce111513e744d27acc0c408d8c5cb9f55e26
-  md5: 5d3cd0031dbfcd800f6f5a4122e84dc7
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-common-0.9.27-h2466b09_0.conda
+  sha256: 6d59ceca0f648ef0e6d6bce00dc3824a9340bf8fbffeafc80d4dbd230860579b
+  md5: 8355fbefc33c680fa1a96ba7d3365dfa
   depends:
   - ucrt >=10.0.20348.0
   - vc >=14.2,<15
@@ -8506,377 +8506,388 @@ packages:
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 233457
-  timestamp: 1722652430792
+  size: 235039
+  timestamp: 1723640170113
 - kind: conda
   name: aws-c-common
-  version: 0.9.25
+  version: 0.9.27
   build: h4bc722e_0
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.25-h4bc722e_0.conda
-  sha256: af6dbf129de978b8960b1d130770878e5052d992232cbdd4ddf99d2ea705f931
-  md5: 8ae3ebc6f412a923466ae673a3b3953f
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.27-h4bc722e_0.conda
+  sha256: b1725a5ec43bcf606d6bdb248312aa51386b30339dd83a1f16edf620fe03d941
+  md5: 817119e8a21a45d325f65d0d54710052
   depends:
   - __glibc >=2.17,<3.0.a0
   - libgcc-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 236289
-  timestamp: 1722651969732
+  size: 236759
+  timestamp: 1723639577027
 - kind: conda
   name: aws-c-common
-  version: 0.9.25
+  version: 0.9.27
   build: h99b78c6_0
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.25-h99b78c6_0.conda
-  sha256: 0a103d849e90a8c9bf036dc97985e585ba376535c06c38633ef9405ef154bd9a
-  md5: 4d8839ae6eff84cc59296b7bc9fae58e
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-common-0.9.27-h99b78c6_0.conda
+  sha256: 6c5a03e6e8436b307c6e36a257ceb24a95338e5d82de48c7462ceb921adadb35
+  md5: b92f3870b54249178462862413137ca1
   depends:
   - __osx >=11.0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 219319
-  timestamp: 1722651998199
+  size: 220718
+  timestamp: 1723639978181
 - kind: conda
   name: aws-c-common
-  version: 0.9.25
+  version: 0.9.27
   build: hfdf4475_0
   subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.25-hfdf4475_0.conda
-  sha256: aa0034bf17d2b24642a9f861852d725f2ea4f9b14fb90ffc06405e1dc52138ab
-  md5: d76bf5c5229ee864ccae59bba017e0a0
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-common-0.9.27-hfdf4475_0.conda
+  sha256: 3420001537d36a20c81c0a832f95feec849bc50cec4429025498498de8c6be0a
+  md5: 3248125bfac52e553ebb6d010176cc1a
   depends:
   - __osx >=10.13
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 225876
-  timestamp: 1722652021377
+  size: 225349
+  timestamp: 1723639748928
 - kind: conda
   name: aws-c-compression
-  version: 0.2.18
-  build: h0240d57_8
-  build_number: 8
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.18-h0240d57_8.conda
-  sha256: 9c7f3e6544987df22f8f5f38e14eac2d0d954659875e928f91a4d83b4857edca
-  md5: f8edfe3c145f2d12c7a94055238e3979
+  version: 0.2.19
+  build: h3e75f19_0
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.19-h3e75f19_0.conda
+  sha256: 5dab25855379b2419411bcc742a5076e407bd8508eb8ef2be517227c0127adde
+  md5: db4cfae54628d9048153505f150a5e45
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
+  - __osx >=10.13
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 22565
-  timestamp: 1723151107259
+  size: 17892
+  timestamp: 1724353855148
 - kind: conda
   name: aws-c-compression
-  version: 0.2.18
-  build: h5b118bc_8
-  build_number: 8
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-compression-0.2.18-h5b118bc_8.conda
-  sha256: c24ac2c84431237423162804bda58d71e2afe0e8d40349b5195d610fa3ff2aca
-  md5: 9abd3cf6a8559f3cd3870aee63007134
+  version: 0.2.19
+  build: h41e72e7_0
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.19-h41e72e7_0.conda
+  sha256: e61ee499ca9db361bca4d8c8f9bf3db439dfc25bd71f1405d6ec97e74699ef3f
+  md5: c0fa07c8ba0434260ee3e6a05d4ddfa4
   depends:
-  - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=11.0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 17991
-  timestamp: 1723150633995
+  size: 18046
+  timestamp: 1724353909848
 - kind: conda
   name: aws-c-compression
-  version: 0.2.18
-  build: h98f14e4_8
-  build_number: 8
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-compression-0.2.18-h98f14e4_8.conda
-  sha256: 688be60bdf4338a15ecd94f1d64e21194064adbdf4d93719c957960a23f8b50b
-  md5: 216c9074a19acb5fccfc69b538ef275d
+  version: 0.2.19
+  build: ha1e9ad3_0
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-compression-0.2.19-ha1e9ad3_0.conda
+  sha256: 468b22529b0e73985f057259930439310e15b0542dc354a2419aa12d52b5d14c
+  md5: 275ed97d3db3f2858a94e8597c777392
   depends:
-  - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 17932
-  timestamp: 1723150687810
+  size: 22443
+  timestamp: 1724354289281
 - kind: conda
   name: aws-c-compression
-  version: 0.2.18
-  build: hc649ecc_8
-  build_number: 8
+  version: 0.2.19
+  build: haa50ccc_0
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-hc649ecc_8.conda
-  sha256: ef8666bd07183f32219e7de3faf26c16d6acdd42769be0bb15b3668d0c89deba
-  md5: b72e4162acb0cf997185fc1da432a63e
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.19-haa50ccc_0.conda
+  sha256: d7cca92ff47e5de9e53ce6ea90186d578883b35d4c665b166ada2754d7786d05
+  md5: 00c38c49d0befb632f686cf67ee8c9f5
   depends:
   - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - libgcc-ng >=12
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - libgcc-ng >=13
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 19009
-  timestamp: 1723150618661
+  size: 19010
+  timestamp: 1724353825002
 - kind: conda
   name: aws-c-event-stream
-  version: 0.4.2
-  build: h04a40c0_20
-  build_number: 20
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h04a40c0_20.conda
-  sha256: 7b0611ec00f0433db9163f1050cc0289abe380f43a499a7b09aed5976e94afa0
-  md5: 3c1a3aa8f0dd19e1a917b39620fda32c
+  version: 0.4.3
+  build: h324d61a_0
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.3-h324d61a_0.conda
+  sha256: 0cef89be364d635d5012e6ef29bf706f8460aa7d7b3c05b208f915c6ad6255b8
+  md5: 1d7762725c8455949ba37406ee5d5788
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=10.13
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
-  - libgcc-ng >=12
-  - libstdcxx-ng >=12
+  - libcxx >=16
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 53682
-  timestamp: 1723177510411
+  size: 46395
+  timestamp: 1724071152312
 - kind: conda
   name: aws-c-event-stream
-  version: 0.4.2
-  build: h330dd76_20
-  build_number: 20
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.2-h330dd76_20.conda
-  sha256: 0580d2999a2ed936345aefd85fec917daef7bf75d0b066760b421692ac4f8f0f
-  md5: 7c396e1c1004e9a968cc77ebf6034a3c
+  version: 0.4.3
+  build: h570d160_0
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.3-h570d160_0.conda
+  sha256: 608225f14f0befcc351860c2961ae9734f7bf097b3ffb88aea69727c65843689
+  md5: 1c121949295cac86798be8f369768d7c
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
+  - libgcc-ng >=12
+  - libstdcxx-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 54724
-  timestamp: 1723177958755
+  size: 53945
+  timestamp: 1724071086055
 - kind: conda
   name: aws-c-event-stream
-  version: 0.4.2
-  build: h671840a_20
-  build_number: 20
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-event-stream-0.4.2-h671840a_20.conda
-  sha256: cf0876ec0c973ec040616ac77b05be1d79d077bf88e058b6f0c498db35b6d601
-  md5: 2eb9cb654f68fafe2cc22afbd2d16212
+  version: 0.4.3
+  build: h79ff00d_0
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.3-h79ff00d_0.conda
+  sha256: bc45ee6a05f45b0ba2a8f014b2ac67e1aa33b98ec2f95286482bd9af37025fc7
+  md5: 05dc0c49ea75ee73735416e2b3612c56
   depends:
-  - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=11.0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
   - libcxx >=16
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 46357
-  timestamp: 1723177593432
+  size: 47322
+  timestamp: 1724071159670
 - kind: conda
   name: aws-c-event-stream
-  version: 0.4.2
-  build: hf1484eb_20
-  build_number: 20
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-event-stream-0.4.2-hf1484eb_20.conda
-  sha256: 229aab7e93d4aa2045761673a2836c124b969b005de45051a0e456d218d594e1
-  md5: c7274d4c42bb06eaee73853eecbaf39f
+  version: 0.4.3
+  build: hf2a634e_0
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-event-stream-0.4.3-hf2a634e_0.conda
+  sha256: af6296494de3a68ac0d2d1fc1e7f2597f8555be419f90d199174e30cf6c6ecd9
+  md5: 91dcbc9c8ae48e5f1ddc4674193cd37b
   depends:
-  - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
-  - libcxx >=16
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 46838
-  timestamp: 1723177620710
+  size: 54484
+  timestamp: 1724071428821
 - kind: conda
   name: aws-c-http
   version: 0.8.7
-  build: h065a16f_2
-  build_number: 2
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h065a16f_2.conda
-  sha256: d93c898fd33d1aba540f3fc67ff4f19c483c111fe65e3a27de224df1ab70bc67
-  md5: d97b893513c9f0e9c3fa65b2e9fac814
+  build: h1c59cda_5
+  build_number: 5
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-h1c59cda_5.conda
+  sha256: c498aa33b2066f04afc61f504dde43bbbcb3322f31ecb6ddca73602739614ab1
+  md5: 0fc88e5bb5f095bdf4129282411c50c9
   depends:
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-compression >=0.2.18,<0.2.19.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-compression >=0.2.19,<0.2.20.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
+  - libgcc-ng >=13
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 183105
-  timestamp: 1723178102709
+  size: 198111
+  timestamp: 1724413702199
 - kind: conda
   name: aws-c-http
   version: 0.8.7
-  build: h22e442e_2
-  build_number: 2
+  build: h3967ab9_5
+  build_number: 5
   subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h22e442e_2.conda
-  sha256: 6ab77886c207d4e983a9edd6da613cae82af880a053dbe05a3eeacb53bbf2744
-  md5: ce32f723ce4caf907ba85670f8152755
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-http-0.8.7-h3967ab9_5.conda
+  sha256: 73bbb0bf25d01db5b728edb60f553e11086c1941d2ea488b7c7e5e1bb49efd57
+  md5: 86d1a89405e70f9a26ce741cbde4b35f
   depends:
   - __osx >=10.13
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-compression >=0.2.18,<0.2.19.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-compression >=0.2.19,<0.2.20.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 164244
-  timestamp: 1723177829809
+  size: 163843
+  timestamp: 1724413805278
 - kind: conda
   name: aws-c-http
   version: 0.8.7
-  build: h624841c_2
-  build_number: 2
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-h624841c_2.conda
-  sha256: 59e70f6872ea01175ea54d5bbef5212c7a68984c06f7c58189db5abf9cde8c24
-  md5: b66a9322db9d8de706976766ba255abf
+  build: h7d4ba70_5
+  build_number: 5
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-http-0.8.7-h7d4ba70_5.conda
+  sha256: 3ca8b4b6594a603e2c89023ce541598b0afbceb3ccd5bcb377b2d0cc64fd04c1
+  md5: b8f007046d97c8a1ff52b70aeaf4c860
   depends:
-  - __osx >=11.0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-compression >=0.2.18,<0.2.19.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-compression >=0.2.19,<0.2.20.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 153361
-  timestamp: 1723177965062
+  size: 182202
+  timestamp: 1724414052622
 - kind: conda
   name: aws-c-http
   version: 0.8.7
-  build: hc9bb02b_2
-  build_number: 2
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.7-hc9bb02b_2.conda
-  sha256: 58a5830a4bb878568b231e3215d2eedfd713e1ee9ea58dbc5e54e1793ea017e5
-  md5: 1fa3da862ddcc9e9b0538fd58273a65e
+  build: ha3a5f06_5
+  build_number: 5
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-http-0.8.7-ha3a5f06_5.conda
+  sha256: 149f4b42f67fd1a585024abdf16f8048976fa7333c7ab71cf6f553e46fd0a557
+  md5: 36c9f6c831a3f1ec5f23990584b94512
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-compression >=0.2.18,<0.2.19.0a0
+  - __osx >=11.0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-compression >=0.2.19,<0.2.20.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
-  - libgcc-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 197213
-  timestamp: 1723177819689
+  size: 152303
+  timestamp: 1724413815046
 - kind: conda
   name: aws-c-io
   version: 0.14.18
-  build: h3095dc9_2
-  build_number: 2
+  build: h43341e6_6
+  build_number: 6
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-h43341e6_6.conda
+  sha256: ff8c95d55410da8788c232c69e441f6fd39cdaee0859f50fb0536b4de6bb12b5
+  md5: af3beb5ed6d55cbf88dee4671e5c616e
+  depends:
+  - __osx >=11.0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  license: Apache-2.0
+  license_family: Apache
+  purls: []
+  size: 137352
+  timestamp: 1724239263673
+- kind: conda
+  name: aws-c-io
+  version: 0.14.18
+  build: h8d4122e_6
+  build_number: 6
   subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h3095dc9_2.conda
-  sha256: 7578e3f11648e3fd0ccd4f65ee09f0d69de55516b891c932a6212a10c694d396
-  md5: 0c3d97fd2baffa30fce84338d3e9bd19
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-io-0.14.18-h8d4122e_6.conda
+  sha256: 99de2be593516ffa646622e3bde3d546d3dc336b40cdd673a2bd5160fb19411e
+  md5: 81818d850fcf9d276dac5e5bcedbb273
   depends:
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - ucrt >=10.0.20348.0
   - vc >=14.2,<15
   - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 161032
-  timestamp: 1723482456101
+  size: 161001
+  timestamp: 1724239605013
 - kind: conda
   name: aws-c-io
   version: 0.14.18
-  build: h3e50d33_2
-  build_number: 2
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-h3e50d33_2.conda
-  sha256: 8b94d550344392429931aa69f16c38cfd2dbb49ce1e5d2705cb10c8b735dd0ee
-  md5: 9b0aca26d579311a918a0f03047fc447
+  build: hf47059d_6
+  build_number: 6
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hf47059d_6.conda
+  sha256: e0c92084f9052b168a06dfa932a8e62c1017c61ef22fbcb773ae31e4424110f7
+  md5: 321eb6b112918ec8a97b5b96af7f38e8
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - libgcc-ng >=12
-  - s2n >=1.5.0,<1.5.1.0a0
+  - __osx >=10.13
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 159102
-  timestamp: 1723481752341
+  size: 139075
+  timestamp: 1724239099835
 - kind: conda
   name: aws-c-io
   version: 0.14.18
-  build: hacc3fab_2
-  build_number: 2
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-io-0.14.18-hacc3fab_2.conda
-  sha256: 9a7c4d4189d636e408ce9e210f7ae96fe609b8105597a0bbc242350e5dc77603
-  md5: 77a821b803d33ad3906afb223eaa7fbf
+  build: hf5b9b93_6
+  build_number: 6
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.18-hf5b9b93_6.conda
+  sha256: 3640f15a08bc22cf0b7321d177535517f16c360f961306d291e006296f111d7d
+  md5: 8fd43c2719355d795f5c7cef11f08ec0
   depends:
-  - __osx >=11.0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - libgcc-ng >=13
+  - s2n >=1.5.1,<1.5.2.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 138078
-  timestamp: 1723481865779
+  size: 159032
+  timestamp: 1724239051
 - kind: conda
-  name: aws-c-io
-  version: 0.14.18
-  build: hddd658a_2
-  build_number: 2
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-io-0.14.18-hddd658a_2.conda
-  sha256: 25b7fb542ad15042d4e42538b4fd3ad98fef5e50ccdd743e03e0474301ad2f71
-  md5: dd6e6d2924fc7fe472908dfdc73aa1ce
+  name: aws-c-mqtt
+  version: 0.10.4
+  build: h64da278_17
+  build_number: 17
+  subdir: osx-arm64
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h64da278_17.conda
+  sha256: 413f0c6518edc16c7f5f52e64835abd5fdc8154889a2696f125a2efb0d3f5481
+  md5: 8b68d3ffa6799ba33340eafd952acae0
   depends:
-  - __osx >=10.13
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __osx >=11.0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-http >=0.8.7,<0.8.8.0a0
+  - aws-c-io >=0.14.18,<0.14.19.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 138461
-  timestamp: 1723481866356
+  size: 118018
+  timestamp: 1723726203478
 - kind: conda
   name: aws-c-mqtt
   version: 0.10.4
-  build: h1efefa3_16
-  build_number: 16
+  build: h827f298_17
+  build_number: 17
   subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h1efefa3_16.conda
-  sha256: 4ca34b6277f62f0cc56da0e9bbff8529d7f749853c4ace9de6dd3833d1aac2e5
-  md5: 1de479e343cd06db94e05062ab9af2e0
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-mqtt-0.10.4-h827f298_17.conda
+  sha256: 9804a9027ff17955953ef934c48e7344db43f8b0d90a7c45fa5afeecb4f8ae9e
+  md5: e623bc494578a42b34d439472e20a9b2
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - ucrt >=10.0.20348.0
@@ -8885,101 +8896,60 @@ packages:
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 157467
-  timestamp: 1723193297143
+  size: 157632
+  timestamp: 1723726738411
 - kind: conda
   name: aws-c-mqtt
   version: 0.10.4
-  build: h3710d3a_16
-  build_number: 16
+  build: h93740dc_17
+  build_number: 17
   subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h3710d3a_16.conda
-  sha256: f5ee04ec57c28aca7523beab62052bb75a6f91e19430f0a8c666cf70f6b07db2
-  md5: 60bb82553164fd9cb10bfafb6b61df65
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-mqtt-0.10.4-h93740dc_17.conda
+  sha256: da059d67450af5aa87583f9ec231ed18ce481666f410e4964ffbde3ca0681a7f
+  md5: 3311cf7d8a088c450af9ee4b22972edd
   depends:
   - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 138348
-  timestamp: 1723192832275
+  size: 139161
+  timestamp: 1723726448430
 - kind: conda
   name: aws-c-mqtt
   version: 0.10.4
-  build: h674cf7e_16
-  build_number: 16
+  build: hc14a930_17
+  build_number: 17
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-h674cf7e_16.conda
-  sha256: 759a15d367ef69f24448ace52843c4999e1795710c4393ea86d7768b1934db93
-  md5: a96a7cd318b665b8784cbe7e67916f66
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hc14a930_17.conda
+  sha256: bc176d82875700e0ee51ddaef188fd6ca2a44b2efc69e0cb434460319216049b
+  md5: f0e3f95a9f545d5975e8573f80cdb5fa
   depends:
   - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - libgcc-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 163774
-  timestamp: 1723192743841
-- kind: conda
-  name: aws-c-mqtt
-  version: 0.10.4
-  build: h8469d54_16
-  build_number: 16
-  subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-mqtt-0.10.4-h8469d54_16.conda
-  sha256: 67ed006fb6910a3837d35585be221f9db3b64699f63396498315994952930c60
-  md5: b48411d5db102d80c0dc828727d72c6e
-  depends:
-  - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-http >=0.8.7,<0.8.8.0a0
-  - aws-c-io >=0.14.18,<0.14.19.0a0
-  license: Apache-2.0
-  license_family: Apache
-  purls: []
-  size: 117964
-  timestamp: 1723192603387
-- kind: conda
-  name: aws-c-s3
-  version: 0.6.4
-  build: h89cdba3_6
-  build_number: 6
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-h89cdba3_6.conda
-  sha256: 2bc2ac953500501abbfd3b8d9baee81ef7f91753733eb782fa488e748c9f6c46
-  md5: 3e2f8edf601210730c6f1613de9539cc
-  depends:
-  - __osx >=10.13
-  - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-http >=0.8.7,<0.8.8.0a0
-  - aws-c-io >=0.14.18,<0.14.19.0a0
-  - aws-checksums >=0.1.18,<0.1.19.0a0
-  license: Apache-2.0
-  license_family: Apache
-  purls: []
-  size: 96890
-  timestamp: 1723207662384
+  size: 163877
+  timestamp: 1723726327641
 - kind: conda
   name: aws-c-s3
   version: 0.6.4
-  build: h9116173_6
-  build_number: 6
+  build: h21392f2_8
+  build_number: 8
   subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h9116173_6.conda
-  sha256: 49ca36fc4bae3cadeb703f4da0a8b0e6f5d5694b617fc1d34268f8537d5baf37
-  md5: 2e508c828a6b222906289373251043d5
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-s3-0.6.4-h21392f2_8.conda
+  sha256: 8137069560d3778e790618a2817eb0c5da4e1a97cd5ff91c778e365b3147b737
+  md5: 15db0136544c378e00fd7158bf019a21
   depends:
   - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
@@ -8989,44 +8959,44 @@ packages:
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 107716
-  timestamp: 1723208085431
+  size: 107820
+  timestamp: 1723739640662
 - kind: conda
   name: aws-c-s3
   version: 0.6.4
-  build: ha2a438e_6
-  build_number: 6
+  build: h3f26dbc_8
+  build_number: 8
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-ha2a438e_6.conda
-  sha256: 529f0d64b4e51377935ca60a0150d4fa1c142c1e3665a46996d3147496947808
-  md5: b45e9b813ec5921fbbbdef529dc97813
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-s3-0.6.4-h3f26dbc_8.conda
+  sha256: 2e9259cd3a8e26162bd2eddc55f83d6af6cefaf8f79e0eaad0fe08d9ae98c835
+  md5: 2a93a85d64e31922f2bd28691fac9556
   depends:
   - __osx >=11.0
   - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 95503
-  timestamp: 1723207735434
+  size: 95938
+  timestamp: 1723739497884
 - kind: conda
   name: aws-c-s3
   version: 0.6.4
-  build: hbe604ca_6
-  build_number: 6
+  build: h558cea2_8
+  build_number: 8
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-hbe604ca_6.conda
-  sha256: c184511cf6d733bbdfc80d68b3087d8743c735d7bb40176cef73d9521c8d921d
-  md5: a57d5bbd0559aed25766420bbd371aa0
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.4-h558cea2_8.conda
+  sha256: 8206b63d89e1cf08a0e4bc4852cb15080bc9754df48acbc5e59fbe2ec50b3da8
+  md5: af03e7b03e929396fb80ffac1a676c89
   depends:
   - __glibc >=2.17,<3.0.a0
   - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
@@ -9035,314 +9005,312 @@ packages:
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 111628
-  timestamp: 1723207613777
+  size: 111536
+  timestamp: 1723739247330
 - kind: conda
-  name: aws-c-sdkutils
-  version: 0.1.19
-  build: h0240d57_0
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-h0240d57_0.conda
-  sha256: 2cd6632ec4fc50e868cb9b11acaa98405283cf7104136bbc16e22ab729cc2f98
-  md5: facd2e7d595af64ab74d0ffd0d70fb4f
+  name: aws-c-s3
+  version: 0.6.4
+  build: hd06a241_8
+  build_number: 8
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-s3-0.6.4-hd06a241_8.conda
+  sha256: 06764f43c21ecad9be0a546d4a8b798e353ede7058674c70ab4d259458b0e42e
+  md5: 192a67ed8897b6cfb844d83da17f3f36
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
+  - __osx >=10.13
+  - aws-c-auth >=0.7.25,<0.7.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-http >=0.8.7,<0.8.8.0a0
+  - aws-c-io >=0.14.18,<0.14.19.0a0
+  - aws-checksums >=0.1.18,<0.1.19.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 54917
-  timestamp: 1723176532090
+  size: 96550
+  timestamp: 1723739435090
 - kind: conda
   name: aws-c-sdkutils
   version: 0.1.19
-  build: h5b118bc_0
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-h5b118bc_0.conda
-  sha256: 4be90827a59b297f04238cf2970ca7039d64c2e06b1a1d7799e332143e8c2e7a
-  md5: 9b41d2dd02f9a56e2fb78e6d7f66fddf
+  build: h038f3f9_2
+  build_number: 2
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-h038f3f9_2.conda
+  sha256: 5612c9cad56662db50a1bcc2d8dca1fe273f7abad6f670fef328e4044beabc75
+  md5: 6861cab6cddb5d713cb3db95c838d30f
   depends:
-  - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - libgcc-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 50982
-  timestamp: 1723176233601
+  size: 55878
+  timestamp: 1723691348466
 - kind: conda
   name: aws-c-sdkutils
   version: 0.1.19
-  build: h98f14e4_0
+  build: h85401af_2
+  build_number: 2
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h98f14e4_0.conda
-  sha256: b121ef5e6343beb24cdb269689101ab11bcd8b3dd379beed251140bf11f395a4
-  md5: 4e46163b63fd4e51dfb93b14cf5756c3
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-c-sdkutils-0.1.19-h85401af_2.conda
+  sha256: faf9f32a7b3f312f370e77cf52e6afe512c6cce4cd9709fe039ff08acd877f5a
+  md5: 23183f9ce785058346cbb89c4327b02b
   depends:
   - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 49936
-  timestamp: 1723176203536
+  size: 49819
+  timestamp: 1723691442488
 - kind: conda
   name: aws-c-sdkutils
   version: 0.1.19
-  build: hc649ecc_0
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.19-hc649ecc_0.conda
-  sha256: dc1eaf29c3a4092ae653b3ce80d5c8dfdc73a20d70c62c25f6a0faff059cd3a1
-  md5: f1d4b80dfc062bc55cecc20148eb925a
+  build: ha1e9ad3_2
+  build_number: 2
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-c-sdkutils-0.1.19-ha1e9ad3_2.conda
+  sha256: c87e00f73686c3b43f0a2d6ff480f28d2a8f07811bad5e406bbe0ca8240bbba4
+  md5: da087023762ab6dc62fd1d374b6cc30f
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - libgcc-ng >=12
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 56083
-  timestamp: 1723176096906
+  size: 54992
+  timestamp: 1723691803596
 - kind: conda
-  name: aws-checksums
-  version: 0.1.18
-  build: h0240d57_8
-  build_number: 8
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-h0240d57_8.conda
-  sha256: 7ee5937729c1ee8fc05022c73423749a0486bbd6fe7f01e958532a5f7ee9d1ed
-  md5: f419be1f5cfd2645fa7f68bf410c8548
+  name: aws-c-sdkutils
+  version: 0.1.19
+  build: hf37c103_2
+  build_number: 2
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-c-sdkutils-0.1.19-hf37c103_2.conda
+  sha256: 7c1d055c1f67e4572de18e9daec81b74f59a6f77c2213746dab3cf12b5be253f
+  md5: a8f45839733a97c206cd5df6945c4a27
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
+  - __osx >=10.13
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 52214
-  timestamp: 1723163987764
+  size: 50490
+  timestamp: 1723691467686
 - kind: conda
   name: aws-checksums
   version: 0.1.18
-  build: h5b118bc_8
-  build_number: 8
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-h5b118bc_8.conda
-  sha256: 03d957215e7277d17742e46b4a11e523f619f4e2a2c80b3323469636cd0b3ff8
-  md5: 1b8bd81a2d96c873877f582215cb0145
+  build: h038f3f9_10
+  build_number: 10
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-h038f3f9_10.conda
+  sha256: a94547ff766fb420c368bb8d4fd1c8d99b13088d176c43ad7bb7458ef47e45bc
+  md5: 4bf9c8fcf2bb6793c55e5c5758b9b011
   depends:
-  - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - __glibc >=2.17,<3.0.a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - libgcc-ng >=12
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 48670
-  timestamp: 1723163853774
+  size: 49839
+  timestamp: 1723691467978
 - kind: conda
   name: aws-checksums
   version: 0.1.18
-  build: h98f14e4_8
-  build_number: 8
+  build: h85401af_10
+  build_number: 10
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h98f14e4_8.conda
-  sha256: 9dc4e7ba552d98d1928da6b6f2aea8187b251d70a9e966cb4abaed32aecc4a42
-  md5: daacf29419901e2f8e75166581ab6551
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-checksums-0.1.18-h85401af_10.conda
+  sha256: aeafa3581c3b82d5ac9b13a041795706c3cb0efe3764ee6825f0042a7f52041e
+  md5: 446c0b024a1cbf4b769d271da2bfdce2
   depends:
   - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 49065
-  timestamp: 1723163892019
+  size: 48964
+  timestamp: 1723691578183
 - kind: conda
   name: aws-checksums
   version: 0.1.18
-  build: hc649ecc_8
-  build_number: 8
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-hc649ecc_8.conda
-  sha256: 63ccb2a0b471dac505e95d6a623b041e00e8a2a734fd33fd101f7d3a0018becf
-  md5: 178504fd14f8486ce3153fa16371ceef
+  build: ha1e9ad3_10
+  build_number: 10
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-checksums-0.1.18-ha1e9ad3_10.conda
+  sha256: d992544ad6ab4fc55bae80bdff9ab6c9d71b021c91297e835e3999b9cab4645c
+  md5: 93c84eec0955253bf07b5fbff95c6200
   depends:
-  - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - libgcc-ng >=12
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 49998
-  timestamp: 1723163750630
+  size: 52177
+  timestamp: 1723691948336
 - kind: conda
-  name: aws-crt-cpp
-  version: 0.27.5
-  build: h53ac995_5
-  build_number: 5
+  name: aws-checksums
+  version: 0.1.18
+  build: hf37c103_10
+  build_number: 10
   subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.5-h53ac995_5.conda
-  sha256: c2dc1f9fd2e19c9d896cb2859e161d82d1d5ba9e4992e70566db8bc14e77863d
-  md5: 39e26358a446c7e2844f1da5e1d6e2c1
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-checksums-0.1.18-hf37c103_10.conda
+  sha256: e42c8e70a71e9bd7a228328a5b51efae0c15bd8ef7ed26fae238461a8335f699
+  md5: 86fb971912f9222b14b0b3e695c52461
   depends:
   - __osx >=10.13
-  - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
-  - aws-c-http >=0.8.7,<0.8.8.0a0
-  - aws-c-io >=0.14.18,<0.14.19.0a0
-  - aws-c-mqtt >=0.10.4,<0.10.5.0a0
-  - aws-c-s3 >=0.6.4,<0.6.5.0a0
-  - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
-  - libcxx >=16
+  - aws-c-common >=0.9.27,<0.9.28.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 290343
-  timestamp: 1723235660535
+  size: 48683
+  timestamp: 1723691504517
 - kind: conda
   name: aws-crt-cpp
-  version: 0.27.5
-  build: h55e8508_5
-  build_number: 5
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.5-h55e8508_5.conda
-  sha256: 3e8cd7ad1e13eb204443f44de13d5e9041c656102da5728c8b030f2c4953d8b5
-  md5: 1432fe717f2d651606acd0f8b52277a9
+  version: 0.27.6
+  build: h1966bd9_0
+  subdir: linux-64
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.6-h1966bd9_0.conda
+  sha256: f45a23fc50e9df01f3235672d805b9175ca05d8966a32351c3fb98970b94d2d1
+  md5: 30b59fa809914489974fe275a0fb7c7e
   depends:
+  - __glibc >=2.17,<3.0.a0
   - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-mqtt >=0.10.4,<0.10.5.0a0
   - aws-c-s3 >=0.6.4,<0.6.5.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.2,<15
-  - vc14_runtime >=14.29.30139
+  - libgcc-ng >=13
+  - libstdcxx-ng >=13
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 254293
-  timestamp: 1723235803498
+  size: 349153
+  timestamp: 1724277524154
 - kind: conda
   name: aws-crt-cpp
-  version: 0.27.5
-  build: h7061aca_5
-  build_number: 5
+  version: 0.27.6
+  build: h3fdcf82_0
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.5-h7061aca_5.conda
-  sha256: 944baaa5c2250025fb8160b9121f5b311e77f6321df02202833692851e8f4756
-  md5: ec979bc1f6b5609e67a1b212a3fd2cf3
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-crt-cpp-0.27.6-h3fdcf82_0.conda
+  sha256: 2dfb60783d944de57ece842fe1a6532a45e41444229d98c25fe9e727098a0d98
+  md5: c78b061e9af81b1122da4dcd00769e35
   depends:
   - __osx >=11.0
   - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-mqtt >=0.10.4,<0.10.5.0a0
   - aws-c-s3 >=0.6.4,<0.6.5.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
-  - libcxx >=16
+  - libcxx >=17
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 229072
-  timestamp: 1723235603851
+  size: 229201
+  timestamp: 1724277538271
 - kind: conda
   name: aws-crt-cpp
-  version: 0.27.5
-  build: hba11562_5
-  build_number: 5
-  subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.5-hba11562_5.conda
-  sha256: 6c2d72591495d68b17d6f428b32517a9b59b710eafdc8200e627780812886e6f
-  md5: 3b97f88120ba6bc6c53b428954a15895
+  version: 0.27.6
+  build: h6fef074_0
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-crt-cpp-0.27.6-h6fef074_0.conda
+  sha256: ae13f758658c97d2769697bff00f1c1f26eb3bbb10e22b3e04141dfc0844a5aa
+  md5: cde08e29f68adaae135c0fefba04c70f
   depends:
-  - __glibc >=2.17,<3.0.a0
   - aws-c-auth >=0.7.25,<0.7.26.0a0
-  - aws-c-cal >=0.7.2,<0.7.3.0a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
   - aws-c-http >=0.8.7,<0.8.8.0a0
   - aws-c-io >=0.14.18,<0.14.19.0a0
   - aws-c-mqtt >=0.10.4,<0.10.5.0a0
   - aws-c-s3 >=0.6.4,<0.6.5.0a0
   - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
-  - libgcc-ng >=12
-  - libstdcxx-ng >=12
+  - ucrt >=10.0.20348.0
+  - vc >=14.2,<15
+  - vc14_runtime >=14.29.30139
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 345928
-  timestamp: 1723235386758
+  size: 253689
+  timestamp: 1724277867217
 - kind: conda
-  name: aws-sdk-cpp
-  version: 1.11.379
-  build: h065e723_2
-  build_number: 2
+  name: aws-crt-cpp
+  version: 0.27.6
+  build: hb7fc533_0
   subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-h065e723_2.conda
-  sha256: 4c35cae6be1dbce28a555b014513f511d47005b98aecb3252f00087215dbbef8
-  md5: b9f139d1a5247fdbcbd2b102433d2960
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-crt-cpp-0.27.6-hb7fc533_0.conda
+  sha256: 05a1a668bf2fcc00a48e50ff5392b511821fe3f2c2c336b8e57432d7d26a3221
+  md5: e3a0881130205bdeccf50bcfb548c8d0
   depends:
   - __osx >=10.13
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
-  - aws-checksums >=0.1.18,<0.1.19.0a0
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
-  - libcurl >=8.9.1,<9.0a0
-  - libcxx >=16
-  - libzlib >=1.3.1,<2.0a0
-  - openssl >=3.3.1,<4.0a0
+  - aws-c-auth >=0.7.25,<0.7.26.0a0
+  - aws-c-cal >=0.7.3,<0.7.4.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
+  - aws-c-http >=0.8.7,<0.8.8.0a0
+  - aws-c-io >=0.14.18,<0.14.19.0a0
+  - aws-c-mqtt >=0.10.4,<0.10.5.0a0
+  - aws-c-s3 >=0.6.4,<0.6.5.0a0
+  - aws-c-sdkutils >=0.1.19,<0.1.20.0a0
+  - libcxx >=17
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 2700929
-  timestamp: 1723471903012
+  size: 291605
+  timestamp: 1724277534195
 - kind: conda
   name: aws-sdk-cpp
   version: 1.11.379
-  build: h0db37eb_2
-  build_number: 2
+  build: h34e68ec_5
+  build_number: 5
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h0db37eb_2.conda
-  sha256: cda6ed6e15cc04b0f513d8009c67fc6c6db4756be7d3f6b732a6fd8bdac3e520
-  md5: d9996884741f0db39dc34be34dd87596
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/aws-sdk-cpp-1.11.379-h34e68ec_5.conda
+  sha256: d00d23735b03beb14a90ab951b53bc22c6c8390698b7ee25e282a8cad7520bdd
+  md5: ecf5bf7cb7d86b8ac38b8ed40e5a0105
   depends:
   - __osx >=11.0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - libcurl >=8.9.1,<9.0a0
-  - libcxx >=16
+  - libcxx >=17
   - libzlib >=1.3.1,<2.0a0
   - openssl >=3.3.1,<4.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 2669037
-  timestamp: 1723472158961
+  size: 2663970
+  timestamp: 1724353774174
 - kind: conda
   name: aws-sdk-cpp
   version: 1.11.379
-  build: h2205d66_2
-  build_number: 2
+  build: hd66f94a_5
+  build_number: 5
   subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-h2205d66_2.conda
-  sha256: 4c64f829f589fdb36e77ecc63f9c289d69052be2b7b4d7a6dfe22d842bc6f0cd
-  md5: 6abcadee194e8becd912c9a0b75eddc0
+  url: https://conda.anaconda.org/conda-forge/win-64/aws-sdk-cpp-1.11.379-hd66f94a_5.conda
+  sha256: 2d37d00e2448402ad30f82ba5081066a625d6ccd5e9fbcffccf32fde3058c1bf
+  md5: 9e944136425de314ea70472ddbf44913
   depends:
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - libzlib >=1.3.1,<2.0a0
   - ucrt >=10.0.20348.0
   - vc >=14.2,<15
@@ -9350,33 +9318,57 @@ packages:
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 2766410
-  timestamp: 1723472583120
+  size: 2780461
+  timestamp: 1724354543886
 - kind: conda
   name: aws-sdk-cpp
   version: 1.11.379
-  build: he20dfa5_2
-  build_number: 2
+  build: hdc2ca5d_5
+  build_number: 5
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/aws-sdk-cpp-1.11.379-hdc2ca5d_5.conda
+  sha256: 93646d937cd26679bbf87c72e929c3d9b61ee0dbd5f304d9eb4ddf3e434e3f2b
+  md5: 4cf64b3ef6d3499e5b3ba3275986fcc8
+  depends:
+  - __osx >=10.13
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
+  - aws-checksums >=0.1.18,<0.1.19.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
+  - libcurl >=8.9.1,<9.0a0
+  - libcxx >=17
+  - libzlib >=1.3.1,<2.0a0
+  - openssl >=3.3.1,<4.0a0
+  license: Apache-2.0
+  license_family: Apache
+  purls: []
+  size: 2708008
+  timestamp: 1724353692217
+- kind: conda
+  name: aws-sdk-cpp
+  version: 1.11.379
+  build: hf9693f6_5
+  build_number: 5
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-he20dfa5_2.conda
-  sha256: a43661e14ad9d51c0c9330effbc86d9960096484181402dd43bd9617c803f67b
-  md5: 25a12549e0aeeaa2718035c3e54ff520
+  url: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.379-hf9693f6_5.conda
+  sha256: 5267e5a24249d89368226caa3a9b64d53e2c8e481650c57755cad90a9d0578bf
+  md5: 18a4bf7e8a65006b26ca53700fcf2362
   depends:
   - __glibc >=2.17,<3.0.a0
-  - aws-c-common >=0.9.25,<0.9.26.0a0
-  - aws-c-event-stream >=0.4.2,<0.4.3.0a0
+  - aws-c-common >=0.9.27,<0.9.28.0a0
+  - aws-c-event-stream >=0.4.3,<0.4.4.0a0
   - aws-checksums >=0.1.18,<0.1.19.0a0
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - libcurl >=8.9.1,<9.0a0
-  - libgcc-ng >=12
-  - libstdcxx-ng >=12
+  - libgcc-ng >=13
+  - libstdcxx-ng >=13
   - libzlib >=1.3.1,<2.0a0
   - openssl >=3.3.1,<4.0a0
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 2917924
-  timestamp: 1723471330992
+  size: 2929723
+  timestamp: 1724353503402
 - kind: conda
   name: azure-core-cpp
   version: 1.13.0
@@ -25534,10 +25526,11 @@ packages:
   name: pandamesh
   version: 0.1.6
   path: .
-  sha256: 7ac11112146adc684a16796ea84c61b7ff53094ecdae9dac7a3bdddd3154ccad
+  sha256: f988582ac65dfe97e56638cb13427fe6a3e2c3e75d0944de88965b7fe555f6f3
   requires_dist:
   - geopandas
   - pooch
+  - scipy
   - shapely>=2.0
   - triangle
   - geopandas ; extra == 'all'
@@ -31869,12 +31862,12 @@ packages:
   timestamp: 1723039388546
 - kind: conda
   name: s2n
-  version: 1.5.0
+  version: 1.5.1
   build: h3400bea_0
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.0-h3400bea_0.conda
-  sha256: 594878a49b1c4d657795f80ffbe87f15a16cd2162f28383a5b794d301d6cbc65
-  md5: 5f17883266c5312a1fc73583f28ebae5
+  url: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.5.1-h3400bea_0.conda
+  sha256: 2717b0fa534aee9aca152ae980731f3d201542d12c19403563aaa07194021041
+  md5: bf136eb7f8e15fcf8915c1a04b0aec6f
   depends:
   - __glibc >=2.17,<3.0.a0
   - libgcc-ng >=12
@@ -31882,8 +31875,8 @@ packages:
   license: Apache-2.0
   license_family: Apache
   purls: []
-  size: 353483
-  timestamp: 1723253710366
+  size: 356808
+  timestamp: 1724194797671
 - kind: conda
   name: scikit-learn
   version: 1.5.1
@@ -33798,15 +33791,15 @@ packages:
 - kind: conda
   name: tiledb
   version: 2.25.0
-  build: h05a3d2a_7
-  build_number: 7
+  build: h183c882_8
+  build_number: 8
   subdir: osx-arm64
-  url: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h05a3d2a_7.conda
-  sha256: 04f157f2a11c3c270076e139a7bc14061baf0a2613cc8a72d3832afc4d8d7ff4
-  md5: 278d6e356a2faa8b70c346c5befb771c
+  url: https://conda.anaconda.org/conda-forge/osx-arm64/tiledb-2.25.0-h183c882_8.conda
+  sha256: dd0e5eec077e32002503b6b2743fb8f2840a0659e43782fba18e47cdfb36a045
+  md5: 587e6411934146745c0a3c7fef7bc1c7
   depends:
   - __osx >=11.0
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - aws-sdk-cpp >=1.11.379,<1.11.380.0a0
   - azure-core-cpp >=1.13.0,<1.13.1.0a0
   - azure-identity-cpp >=1.8.0,<1.8.1.0a0
@@ -33817,7 +33810,7 @@ packages:
   - libabseil * cxx17*
   - libabseil >=20240116.2,<20240117.0a0
   - libcurl >=8.9.1,<9.0a0
-  - libcxx >=16
+  - libcxx >=17
   - libgoogle-cloud >=2.28.0,<2.29.0a0
   - libgoogle-cloud-storage >=2.28.0,<2.29.0a0
   - libwebp-base >=1.4.0,<2.0a0
@@ -33829,20 +33822,20 @@ packages:
   license: MIT
   license_family: MIT
   purls: []
-  size: 3528513
-  timestamp: 1723475230910
+  size: 3547421
+  timestamp: 1724371658393
 - kind: conda
   name: tiledb
   version: 2.25.0
-  build: h213c483_7
-  build_number: 7
+  build: h33a2d6f_8
+  build_number: 8
   subdir: linux-64
-  url: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h213c483_7.conda
-  sha256: 3d60651162fc1bd178791ad8b877e5dbadfdb34c6786cc5308a83e375959f639
-  md5: 9d8f1988a2d0420abf75e06497667594
+  url: https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.25.0-h33a2d6f_8.conda
+  sha256: 97e0dff22379b597882a22e03f54f509a07adae8dc857b1c1678ea17b3244db8
+  md5: 6fee617e35ff1545510ca2eaee69d341
   depends:
   - __glibc >=2.17,<3.0.a0
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - aws-sdk-cpp >=1.11.379,<1.11.380.0a0
   - azure-core-cpp >=1.13.0,<1.13.1.0a0
   - azure-identity-cpp >=1.8.0,<1.8.1.0a0
@@ -33853,10 +33846,10 @@ packages:
   - libabseil * cxx17*
   - libabseil >=20240116.2,<20240117.0a0
   - libcurl >=8.9.1,<9.0a0
-  - libgcc-ng >=12
+  - libgcc-ng >=13
   - libgoogle-cloud >=2.28.0,<2.29.0a0
   - libgoogle-cloud-storage >=2.28.0,<2.29.0a0
-  - libstdcxx-ng >=12
+  - libstdcxx-ng >=13
   - libwebp-base >=1.4.0,<2.0a0
   - libzlib >=1.3.1,<2.0a0
   - lz4-c >=1.9.3,<1.10.0a0
@@ -33866,19 +33859,20 @@ packages:
   license: MIT
   license_family: MIT
   purls: []
-  size: 4366120
-  timestamp: 1723475383496
+  size: 4538440
+  timestamp: 1724371764828
 - kind: conda
   name: tiledb
   version: 2.25.0
-  build: h3c7d8a4_7
-  build_number: 7
-  subdir: win-64
-  url: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-h3c7d8a4_7.conda
-  sha256: c919786f19f11c7ab37605255968dd400ad311bba26f8c40011ecd0fbfcde281
-  md5: df07dcb982b6e5d8a0244afc40df9542
+  build: h68e3656_8
+  build_number: 8
+  subdir: osx-64
+  url: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h68e3656_8.conda
+  sha256: c1e3af0f4069f11178c7b62dadb010e0362218a9a0148421f9d6615a27a158ce
+  md5: 9b54ee4771aed7e056df8e90fb165e24
   depends:
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - __osx >=10.13
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - aws-sdk-cpp >=1.11.379,<1.11.380.0a0
   - azure-core-cpp >=1.13.0,<1.13.1.0a0
   - azure-identity-cpp >=1.8.0,<1.8.1.0a0
@@ -33888,8 +33882,8 @@ packages:
   - fmt >=11.0.2,<12.0a0
   - libabseil * cxx17*
   - libabseil >=20240116.2,<20240117.0a0
-  - libcrc32c >=1.1.2,<1.2.0a0
   - libcurl >=8.9.1,<9.0a0
+  - libcxx >=17
   - libgoogle-cloud >=2.28.0,<2.29.0a0
   - libgoogle-cloud-storage >=2.28.0,<2.29.0a0
   - libwebp-base >=1.4.0,<2.0a0
@@ -33897,27 +33891,23 @@ packages:
   - lz4-c >=1.9.3,<1.10.0a0
   - openssl >=3.3.1,<4.0a0
   - spdlog >=1.14.1,<1.15.0a0
-  - ucrt >=10.0.20348.0
-  - vc >=14.3,<15
-  - vc14_runtime >=14.40.33810
   - zstd >=1.5.6,<1.6.0a0
   license: MIT
   license_family: MIT
   purls: []
-  size: 3107503
-  timestamp: 1723475894671
+  size: 3889943
+  timestamp: 1724371788355
 - kind: conda
   name: tiledb
   version: 2.25.0
-  build: h6b8956e_7
-  build_number: 7
-  subdir: osx-64
-  url: https://conda.anaconda.org/conda-forge/osx-64/tiledb-2.25.0-h6b8956e_7.conda
-  sha256: 61602664840a7aa8d457ce3aa26c78bb3f11759e8ba65c90e6c8eb590830f662
-  md5: 80b5d1a748253dcc580c8754f7b5d580
+  build: hc6d05bc_8
+  build_number: 8
+  subdir: win-64
+  url: https://conda.anaconda.org/conda-forge/win-64/tiledb-2.25.0-hc6d05bc_8.conda
+  sha256: 4b5452f23b6639f6ad9709a43383a7fe81f463b239e170893db78eb411d65f55
+  md5: a2691392f6d4bbbd1d7f81b5756669f8
   depends:
-  - __osx >=10.13
-  - aws-crt-cpp >=0.27.5,<0.27.6.0a0
+  - aws-crt-cpp >=0.27.6,<0.27.7.0a0
   - aws-sdk-cpp >=1.11.379,<1.11.380.0a0
   - azure-core-cpp >=1.13.0,<1.13.1.0a0
   - azure-identity-cpp >=1.8.0,<1.8.1.0a0
@@ -33927,8 +33917,8 @@ packages:
   - fmt >=11.0.2,<12.0a0
   - libabseil * cxx17*
   - libabseil >=20240116.2,<20240117.0a0
+  - libcrc32c >=1.1.2,<1.2.0a0
   - libcurl >=8.9.1,<9.0a0
-  - libcxx >=16
   - libgoogle-cloud >=2.28.0,<2.29.0a0
   - libgoogle-cloud-storage >=2.28.0,<2.29.0a0
   - libwebp-base >=1.4.0,<2.0a0
@@ -33936,12 +33926,15 @@ packages:
   - lz4-c >=1.9.3,<1.10.0a0
   - openssl >=3.3.1,<4.0a0
   - spdlog >=1.14.1,<1.15.0a0
+  - ucrt >=10.0.20348.0
+  - vc >=14.3,<15
+  - vc14_runtime >=14.40.33810
   - zstd >=1.5.6,<1.6.0a0
   license: MIT
   license_family: MIT
   purls: []
-  size: 3895254
-  timestamp: 1723475275540
+  size: 3119046
+  timestamp: 1724372506251
 - kind: conda
   name: tinycss2
   version: 1.3.0
diff --git a/pyproject.toml b/pyproject.toml
index 066e57d..5f1f7ab 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,6 +15,7 @@ dependencies = [
     'pooch',
     'triangle',
     'shapely >= 2.0',
+    'scipy',
 ]
 classifiers = [
     'Development Status :: 3 - Alpha',
@@ -59,6 +60,7 @@ profile = "black"
 [tool.coverage.report]
 exclude_lines = [
   "pragma: no cover",
+   "@abc.abstractmethod",
 ]
 
 [tool.pixi.project]
@@ -81,6 +83,7 @@ pydata-sphinx-theme = "*"
 py-triangle = "*"
 pytest = "*"
 pytest-cov = "*"
+scipy = "*"
 shapely = ">=2.0"
 sphinx = "*"
 sphinx-gallery = "*"
@@ -131,9 +134,9 @@ ignore = [
     "PD901",
     "PD003",
     "PD004",
+    "PD011",
 ]
-fixable = ["I"]
 ignore-init-module-imports = true
 
 [tool.ruff.lint.pydocstyle]
-convention = "numpy"
\ No newline at end of file
+convention = "numpy"


Test patch:
diff --git a/tests/test_common.py b/tests/test_common.py
index df008b1..3dd281d 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -1,6 +1,3 @@
-import textwrap
-from enum import Enum
-
 import geopandas as gpd
 import numpy as np
 import pytest
@@ -8,13 +5,6 @@
 
 from pandamesh import common
 
-
-class Color(Enum):
-    RED = 1
-    GREEN = 2
-    BLUE = 3
-
-
 a = sg.Polygon(
     [
         (0.0, 0.0),
@@ -109,25 +99,36 @@ def test_flatten():
     assert common.flatten([[1], [2, 3]]) == [1, 2, 3]
 
 
-def test_show_options():
-    common._show_options(Color) == textwrap.dedent(
-        """
-        RED
-        GREEN
-        BLUE
-    """
-    )
+def test_flatten_geometry():
+    point = sg.Point(0, 0)
+    assert common.flatten_geometry(point) == [point]
 
+    multi_point = sg.MultiPoint([sg.Point(0, 0), sg.Point(1, 1)])
+    assert common.flatten_geometry(multi_point) == list(multi_point.geoms)
 
-def test_invalid_option():
-    common.invalid_option("YELLOW", Color) == textwrap.dedent(
-        """
-        Invalid option: YELLOW. Valid options are:
-        RED
-        GREEN
-        BLUE
-    """
-    )
+    point = sg.Point(0, 0)
+    line = sg.LineString([(1, 1), (2, 2)])
+    polygon = sg.Polygon([(3, 3), (4, 4), (4, 3)])
+    multi_point = sg.MultiPoint([sg.Point(5, 5), sg.Point(6, 6)])
+
+    collection = sg.GeometryCollection([point, line, polygon, multi_point])
+    expected = [point, line, polygon, sg.Point(5, 5), sg.Point(6, 6)]
+    assert common.flatten_geometry(collection) == expected
+
+    inner_collection = sg.GeometryCollection([point, multi_point])
+    nested_collection = sg.GeometryCollection([inner_collection, line, polygon])
+    expected = [point, sg.Point(5, 5), sg.Point(6, 6), line, polygon]
+    assert common.flatten_geometry(nested_collection) == expected
+
+    # Also test flatten_geometries
+    geometries = [
+        point,
+        sg.GeometryCollection([multi_point]),
+        sg.GeometryCollection([line, polygon]),
+    ]
+    actual = common.flatten_geometries(geometries)
+    assert isinstance(actual, np.ndarray)
+    assert all(actual == expected)
 
 
 def test_check_geodataframe():
@@ -155,25 +156,6 @@ def test_check_geodataframe():
     common.check_geodataframe(gdf)
 
 
-def test_overlap_shortlist():
-    # b overlaps with a
-    polygons = gpd.GeoSeries(data=[a, b, d], index=[0, 1, 2])
-    ia, ib = common.overlap_shortlist(polygons)
-    assert np.array_equal(ia, [0])
-    assert np.array_equal(ib, [1])
-
-    # a touches c, c touches d: should count as overlap for shortlist.
-    polygons = gpd.GeoSeries(data=[a, c, d], index=[0, 1, 2])
-    ia, ib = common.overlap_shortlist(polygons)
-    assert np.array_equal(ia, [0, 1])
-    assert np.array_equal(ib, [1, 2])
-
-    linestrings = gpd.GeoSeries(data=[La, Lb, Lc], index=[0, 1, 2])
-    ia, ib = common.overlap_shortlist(linestrings)
-    assert np.array_equal(ia, [0])
-    assert np.array_equal(ib, [1])
-
-
 def test_intersecting_features():
     polygons = gpd.GeoSeries(data=[a, b, c, d], index=[0, 1, 2, 3])
     ia, ib = common.intersecting_features(polygons, "polygon")
@@ -252,3 +234,71 @@ def test_separate():
     dfs = common.separate(gdf)
     for df in dfs:
         assert np.issubdtype(df["cellsize"].dtype, np.floating)
+
+    with pytest.raises(TypeError, match="Geometry should be one of"):
+        gdf = gpd.GeoDataFrame(geometry=[sg.MultiPolygon([a, b])])
+        common.separate(gdf)
+
+
+def test_grouper():
+    a = np.array([1, 2, 3, 4])
+    a_index = np.array([0, 0, 1, 1])
+    b = np.array([10, 20, 30, 40, 50])
+    b_index = np.array([0, 1, 2, 3])
+    with pytest.raises(ValueError, match="All arrays must be of the same length"):
+        grouper = common.Grouper(a, [0, 0, 1], b, b_index)
+
+    grouper = common.Grouper(a, a_index, b, b_index)
+
+    # First group
+    a_val, b_vals = next(grouper)
+    assert a_val == 1
+    assert np.array_equal(b_vals, np.array([10, 20]))
+
+    # Second group
+    a_val, b_vals = next(grouper)
+    assert a_val == 2
+    assert np.array_equal(b_vals, np.array([30, 40]))
+
+    # As iterator
+    grouper = common.Grouper(a, a_index, b, b_index)
+    results = list(grouper)
+    assert results[0][0] == 1
+    assert np.array_equal(results[0][1], np.array([10, 20]))
+    assert results[1][0] == 2
+    assert np.array_equal(results[1][1], np.array([30, 40]))
+
+
+def test_to_geodataframe():
+    # Two triangles
+    vertices = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
+    faces = np.array([[0, 1, 2], [1, 3, 2]])
+
+    gdf = common.to_geodataframe(vertices, faces)
+    assert isinstance(gdf, gpd.GeoDataFrame)
+    assert len(gdf == 2)
+
+    # Check the coordinates of the first triangle
+    assert np.allclose(
+        np.array(gdf.geometry[0].exterior.coords),
+        np.array([(0, 0), (1, 0), (0, 1), (0, 0)]),
+    )
+
+    # Check the coordinates of the second triangle
+    assert np.allclose(
+        np.array(gdf.geometry[1].exterior.coords),
+        np.array([(1, 0), (1, 1), (0, 1), (1, 0)]),
+    )
+
+    # Define vertices
+    vertices = np.array([[0, 0], [1, 0], [0, 1], [1, 1], [2, 0.5]], dtype=float)
+
+    # Define faces (triangle: 0,1,2 and quadrangle: 1,3,4,2)
+    faces = np.array([[0, 1, 2, -1], [1, 3, 4, 2]], dtype=int)
+
+    # Call the function
+    gdf = common.to_geodataframe(vertices, faces)
+
+    # Assertions
+    assert isinstance(gdf, gpd.GeoDataFrame)
+    assert len(gdf) == 2  # Two polygons (one triangle, one quadrangle)
diff --git a/tests/test_enum.py b/tests/test_enum.py
new file mode 100644
index 0000000..13b5295
--- /dev/null
+++ b/tests/test_enum.py
@@ -0,0 +1,38 @@
+import re
+import textwrap
+
+import pytest
+
+from pandamesh.enum_base import FlexibleEnum, _show_options
+
+
+class Color(FlexibleEnum):
+    RED = 1
+    GREEN = 2
+    BLUE = 3
+
+
+def test_show_options():
+    _show_options(Color) == textwrap.dedent(
+        """
+        * RED
+        * GREEN
+        * BLUE
+    """
+    )
+
+
+def test_check_options():
+    assert Color(1) == Color.RED
+    assert Color(Color.RED) == Color.RED
+    assert Color.from_value(Color.RED) == Color.RED
+    assert Color.from_value("RED") == Color.RED
+    expected = (
+        "'YELLOW' is not a valid Color. Valid options are:\n * RED\n * GREEN\n * BLUE"
+    )
+    with pytest.raises(ValueError, match=re.escape(expected)):
+        Color.from_value("YELLOW")
+
+    expected = expected.replace("'YELLOW'", "0")
+    with pytest.raises(ValueError, match=re.escape(expected)):
+        Color.from_value(0)
diff --git a/tests/test_meshers.py b/tests/test_meshers.py
index 2329894..57d2200 100644
--- a/tests/test_meshers.py
+++ b/tests/test_meshers.py
@@ -12,7 +12,23 @@
 outer = sg.LinearRing(outer_coords)
 line = sg.LineString(line_coords)
 donut = sg.Polygon(outer, holes=[inner])
-refined = sg.Polygon(inner_coords)
+
+other_inner_coords = np.array([(3.0, 4.0), (7.0, 4.0), (7.0, 6.0), (3.0, 6.0)])
+other_inner = sg.Polygon(other_inner_coords)
+other_hole_coords = np.array(
+    [
+        (7.0, 3.0),
+        (7.0, 4.0),
+        (7.0, 6.0),
+        (7.0, 7.0),
+        (3.0, 7.0),
+        (3.0, 6.0),
+        (3.0, 4.0),
+        (3.0, 3.0),
+    ]
+)
+other_hole = sg.LinearRing(other_hole_coords)
+other_donut = sg.Polygon(outer, holes=[other_hole])
 
 
 def area(vertices, triangles):
@@ -36,6 +52,13 @@ def gmsh_generate(gdf: gpd.GeoDataFrame):
     return mesher.generate()
 
 
+@pytest.mark.parametrize("generate", [triangle_generate, gmsh_generate])
+def test_empty(generate):
+    gdf = gpd.GeoDataFrame(geometry=[line], data={"cellsize": [1.0]})
+    with pytest.raises(ValueError, match="No polygons provided"):
+        generate(gdf)
+
+
 @pytest.mark.parametrize("generate", [triangle_generate, gmsh_generate])
 def test_basic(generate):
     polygon = sg.Polygon(outer)
@@ -55,6 +78,15 @@ def test_hole(generate):
     assert np.allclose(mesh_area, donut.area)
 
 
+@pytest.mark.parametrize("generate", [triangle_generate, gmsh_generate])
+def test_partial_hole(generate):
+    gdf = gpd.GeoDataFrame(geometry=[other_donut, other_inner])
+    gdf["cellsize"] = 1.0
+    vertices, triangles = generate(gdf)
+    mesh_area = area(vertices, triangles).sum()
+    assert np.allclose(mesh_area, other_donut.area + other_inner.area)
+
+
 @pytest.mark.parametrize("generate", [triangle_generate, gmsh_generate])
 def test_adjacent_donut(generate):
     inner_coords2 = inner_coords.copy()
@@ -115,6 +147,21 @@ def test_triangle_properties():
     # Check whether the repr method works
     assert isinstance(mesher.__repr__(), str)
 
+    with pytest.raises(TypeError):
+        mesher.conforming_delaunay = "a"
+
+    with pytest.raises(TypeError):
+        mesher.suppress_exact_arithmetic = "a"
+
+    with pytest.raises(TypeError):
+        mesher.maximum_steiner_points = "a"
+
+    with pytest.raises(ValueError):
+        mesher.delaunay_algorithm = "a"
+
+    with pytest.raises(TypeError):
+        mesher.consistency_check = "a"
+
 
 def test_gmsh_properties():
     gdf = gpd.GeoDataFrame(geometry=[donut])
@@ -140,6 +187,42 @@ def test_gmsh_properties():
     assert mesher.field_combination == pm.FieldCombination.MEAN
     assert mesher.subdivision_algorithm == pm.SubdivisionAlgorithm.BARYCENTRIC
 
+    with pytest.raises(ValueError):
+        mesher.mesh_algorithm = "a"
+
+    with pytest.raises(TypeError):
+        mesher.recombine_all = "a"
+
+    with pytest.raises(TypeError):
+        mesher.force_geometry = "a"
+
+    with pytest.raises(TypeError):
+        mesher.mesh_size_extend_from_boundary = "a"
+
+    with pytest.raises(TypeError):
+        mesher.mesh_size_from_points = "a"
+
+    with pytest.raises(TypeError):
+        mesher.mesh_size_from_curvature = "a"
+
+    with pytest.raises(ValueError):
+        mesher.field_combination = "a"
+
+    with pytest.raises(ValueError):
+        mesher.subdivision_algorithm = "a"
+
+    with pytest.raises(ValueError):
+        mesher.general_verbosity = "a"
+
+
+def test_gmsh_write(tmp_path):
+    gdf = gpd.GeoDataFrame(geometry=[donut])
+    gdf["cellsize"] = 1.0
+    mesher = pm.GmshMesher(gdf)
+    path = tmp_path / "a.msh"
+    mesher.write(path)
+    assert path.exists()
+
 
 @pytest.mark.parametrize("read_config_files", [True, False])
 @pytest.mark.parametrize("interruptible", [True, False])
@@ -152,3 +235,23 @@ def test_gmsh_initialization_kwargs(read_config_files, interruptible):
     vertices, triangles = mesher.generate()
     mesh_area = area(vertices, triangles).sum()
     assert np.allclose(mesh_area, donut.area)
+
+
+def test_generate_geodataframe():
+    gdf = gpd.GeoDataFrame(geometry=[donut])
+    gdf["cellsize"] = 1.0
+
+    result = pm.TriangleMesher(gdf).generate_geodataframe()
+    assert isinstance(result, gpd.GeoDataFrame)
+    assert np.allclose(result.area.sum(), donut.area)
+
+    mesher = pm.GmshMesher(gdf)
+    result = mesher.generate_geodataframe()
+    assert isinstance(result, gpd.GeoDataFrame)
+    assert np.allclose(result.area.sum(), donut.area)
+
+    # Make sure the geodataframe logic can deal with quads as well.
+    mesher.recombine_all = True
+    result = mesher.generate_geodataframe()
+    assert isinstance(result, gpd.GeoDataFrame)
+    assert np.allclose(result.area.sum(), donut.area)
diff --git a/tests/test_preprocessor.py b/tests/test_preprocessor.py
new file mode 100644
index 0000000..c8c6deb
--- /dev/null
+++ b/tests/test_preprocessor.py
@@ -0,0 +1,352 @@
+import geopandas as gpd
+import numpy as np
+import pytest
+import shapely
+import shapely.geometry as sg
+
+from pandamesh import preprocessor as pr
+
+outer = sg.Polygon(
+    [
+        [0.0, 0.0],
+        [10.0, 0.0],
+        [10.0, 10.0],
+        [0.0, 10.0],
+    ]
+)
+inner = sg.Polygon(
+    [
+        [5.0, 2.0],
+        [8.0, 5.0],
+        [5.0, 8.0],
+        [2.0, 5.0],
+    ]
+)
+
+first = sg.Polygon(
+    [
+        [0.0, 0.0],
+        [10.0, 0.0],
+        [10.0, 10.0],
+        [0.0, 10.0],
+    ]
+)
+second = sg.Polygon(
+    [
+        [10.0, 2.0],
+        [18.0, 2.0],
+        [18.0, 8.0],
+        [10.0, 8.0],
+    ]
+)
+third = sg.Polygon(
+    [
+        [18.0, 2.0],
+        [22.0, 2.0],
+        [22.0, 8.0],
+        [18.0, 8.0],
+    ]
+)
+
+donut = sg.Polygon(
+    [
+        [0.0, 0.0],
+        [10.0, 0.0],
+        [10.0, 10.0],
+        [0.0, 10.0],
+    ],
+    holes=[
+        [
+            [2.0, 5.0],
+            [5.0, 8.0],
+            [8.0, 5.0],
+            [5.0, 2.0],
+        ]
+    ],
+)
+
+first_line = shapely.LineString(
+    [
+        [-5.0, 5.0],
+        [6.0, 5.0],
+    ]
+)
+second_line = shapely.LineString(
+    [
+        [5.0, -5.0],
+        [5.0, 6.0],
+    ]
+)
+
+
+def test_collect_exteriors():
+    polygons = np.array([outer, donut])
+    exteriors = pr.collect_exteriors(polygons)
+    assert len(exteriors) == 2
+    assert (shapely.get_type_id(exteriors) == 2).all()
+
+
+def test_collect_interiors():
+    polygons = np.array([donut, outer])
+    interiors = pr.collect_interiors(polygons)
+    assert len(interiors == 1)
+    assert (shapely.get_type_id(interiors) == 2).all()
+
+
+def test_filter_holes_and_assign_values():
+    # Assume: two polygons. Union, then polygonized to create a partitions. Any
+    # polygon found in a hole should disappear.
+    polygons = np.array([donut, inner, second, third])
+    # Overlap between the original outer and inner polygon.
+    index_original = np.array([0, 1, 0, 1])
+    index_union = np.array([0, 0, 1, 1])
+    indexer = np.array([4, 3, 2, 1])
+
+    filtered_polygons, filtered_indexer = pr.filter_holes_and_assign_values(
+        polygons, index_original, index_union, indexer, ascending=True
+    )
+    assert all(filtered_polygons == [donut, inner])
+    assert np.array_equal(filtered_indexer, [3, 3])
+
+    filtered_polygons, filtered_indexer = pr.filter_holes_and_assign_values(
+        polygons, index_original, index_union, indexer, ascending=False
+    )
+    assert all(filtered_polygons == [donut, inner])
+    assert np.array_equal(filtered_indexer, [4, 4])
+
+
+def test_locate_polygons():
+    new = np.array([donut, inner])
+    old = np.array([outer, inner])
+    indexer = np.array([1, 0])
+    polygons, indexer = pr.locate_polygons(new, old, indexer, True)
+    assert all(polygons == [donut, inner])
+    assert np.array_equal(indexer, [1, 0])
+
+    polygons, indexer = pr.locate_polygons(new, old, indexer, False)
+    assert all(polygons == [donut, inner])
+    assert np.array_equal(indexer, [1, 1])
+
+
+def test_locate_lines():
+    old = np.array([first_line, second_line])
+    new = np.array(
+        [
+            shapely.LineString([[-5.0, 5.0], [0.0, 5.0]]),
+            shapely.LineString([[5.0, -5.0], [5.0, 0.0]]),
+            shapely.LineString([[5.0, 0.0], [5.0, 5.0]]),
+            shapely.LineString([[0.0, 5.0], [5.0, 5.0]]),
+        ]
+    )
+    indexer = np.array([0, 1])
+    located = pr.locate_lines(new, old, indexer)
+    assert np.array_equal(located, [0, 1, 1, 0])
+
+
+def test_merge_polyons():
+    polygons = np.array([first, second])
+    merged = pr.merge_polygons(polygons, None)
+    assert len(merged == 1)
+    assert (shapely.get_type_id(merged) == 3).all()
+
+    polygons = np.array([inner, second])
+    merged = pr.merge_polygons(polygons, None)
+    assert len(merged == 2)
+    assert (shapely.get_type_id(merged) == 3).all()
+
+
+def test_separate():
+    point = sg.Point([0.0, 0.0])
+    ring = inner.exterior
+
+    bad = np.array([sg.MultiPolygon([inner, second]), point])
+    with pytest.raises(TypeError, match="Geometry should be one of"):
+        pr.separate(bad)
+
+    geometry = np.array(
+        [
+            point,
+            outer,
+            first_line,
+            ring,
+            inner,
+            second_line,
+        ]
+    )
+    polygons, lines, points = pr.separate(geometry)
+    assert all(polygons == [outer, inner])
+    assert all(lines == [first_line, ring, second_line])
+    assert all(points == [point])
+
+
+def test_preprocessor_init():
+    geometry = [outer]
+    values = [1.0, 2.0]
+
+    with pytest.raises(ValueError, match="geometry and values shape mismatch"):
+        pr.Preprocessor(geometry, values)
+
+    point = sg.Point([1.0, 1.0])
+    geometry = [outer, inner, second, first_line, point]
+    p = pr.Preprocessor(geometry)
+    assert p.values is None
+    assert isinstance(p.polygons, np.ndarray)
+    assert isinstance(p.lines, np.ndarray)
+    assert isinstance(p.points, np.ndarray)
+    assert np.array_equal(p.polygon_indexer, [0, 1, 2])
+    assert np.array_equal(p.line_indexer, [3])
+    assert np.array_equal(p.point_indexer, [4])
+
+    values = [3.0, 2.0, 1.0, 0.5, 0.5]
+    p = pr.Preprocessor(geometry, values)
+    assert np.array_equal(p.values, [0.5, 1.0, 2.0, 3.0])
+    assert np.array_equal(p.polygon_indexer, [3, 2, 1])
+    assert np.array_equal(p.line_indexer, [0])
+    assert np.array_equal(p.point_indexer, [0])
+
+
+class TestPreprocessor:
+    @pytest.fixture(autouse=True)
+    def setup(self):
+        points = [sg.Point([0.1, 0.1]), sg.Point([1.0, 1.0]), sg.Point([-5.0, 5.0])]
+        self.geometry = [outer, inner, second, first_line, second_line, *points]
+        self.values = [3.0, 1.0, 3.0, 0.5, 0.5, 1.0, 1.0, 1.0]
+        self.p = pr.Preprocessor(self.geometry)
+        self.vp = pr.Preprocessor(self.geometry, self.values)
+
+    def test_copy_with(self):
+        def test_value_equality(old, new):
+            for k, v in new.items():
+                assert v is old[k]
+
+        p = self.p
+        copied = p._copy_with()
+        assert isinstance(copied, pr.Preprocessor)
+        assert copied is not p
+        test_value_equality(p.__dict__, copied.__dict__)
+
+        copied = p._copy_with(values=[1, 2, 3, 4, 5, 6, 7])
+        assert p.values is not copied.values
+        new = copied.__dict__
+        assert np.array_equal(new.pop("values"), [1, 2, 3, 4, 5, 6, 7])
+        test_value_equality(p.__dict__, new)
+
+    def test_to_geodataframe(self):
+        gdf = self.p.to_geodataframe()
+        assert isinstance(gdf, gpd.GeoDataFrame)
+        assert set(gdf.columns) == {"geometry", "indexer"}
+        assert np.array_equal(gdf["indexer"], [0, 1, 2, 3, 4, 5, 6, 7])
+
+        gdf = self.vp.to_geodataframe()
+        assert isinstance(gdf, gpd.GeoDataFrame)
+        assert set(gdf.columns) == {"geometry", "indexer", "values"}
+        assert np.array_equal(gdf["values"], self.values)
+
+    def test_merge_polygons(self):
+        merged = self.p.merge_polygons()
+        assert isinstance(merged, pr.Preprocessor)
+        assert len(merged.polygons == self.p.polygons)
+        assert (shapely.get_type_id(merged.polygons) == 3).all()
+
+        merged = self.vp.merge_polygons()
+        assert len(merged.polygons) == 2
+        assert np.array_equal(merged.polygon_indexer, [1, 2])
+        assert (shapely.get_type_id(merged.polygons) == 3).all()
+
+    def test_unify_polygons(self):
+        unified = self.p.unify_polygons()
+        assert isinstance(unified, pr.Preprocessor)
+        assert len(unified.polygons) == 5
+        assert (shapely.get_type_id(unified.polygons) == 3).all()
+        assert np.array_equal(unified.polygon_indexer, [0, 0, 2, 0, 0])
+
+        # Outer and second have same values, so they are merged.
+        unified = self.vp.unify_polygons().merge_polygons()
+        assert len(unified.polygons) == 2
+        assert (shapely.get_type_id(unified.polygons) == 3).all()
+        assert np.array_equal(unified.polygon_indexer, [1, 2])
+
+    def test_clip_lines(self):
+        clipped = self.p.clip_lines()
+        assert isinstance(clipped, pr.Preprocessor)
+        assert len(clipped.lines) == 2
+
+        expected = [
+            sg.LineString([[0.0, 5.0], [6.0, 5.0]]),
+            sg.LineString([[5.0, 0.0], [5.0, 6.0]]),
+        ]
+        assert all(clipped.lines == expected)
+        assert np.array_equal(clipped.line_indexer, [3, 4])
+
+        p = pr.Preprocessor(geometry=[donut, first_line, second_line])
+        clipped = p.clip_lines(distance=0.5)
+        expected = [
+            sg.LineString([[0.5, 5.0], [1.5, 5.0]]),
+            sg.LineString([[5.0, 0.5], [5.0, 1.5]]),
+        ]
+        assert all(clipped.lines == expected)
+        assert np.array_equal(clipped.line_indexer, [1, 2])
+
+    def test_unify_lines(self):
+        unified = self.p.unify_lines()
+        assert isinstance(unified, pr.Preprocessor)
+        assert len(unified.lines) == 4
+        expected = [
+            sg.LineString([[-5.0, 5.0], [5.0, 5.0]]),
+            sg.LineString([[5.0, 5.0], [6.0, 5.0]]),
+            sg.LineString([[5.0, -5.0], [5.0, 5.0]]),
+            sg.LineString([[5.0, 5.0], [5.0, 6.0]]),
+        ]
+        assert all(unified.lines == expected)
+        assert np.array_equal(unified.line_indexer, [3, 3, 4, 4])
+
+    def test_clip_points(self):
+        clipped = self.p.clip_points()
+        assert isinstance(clipped, pr.Preprocessor)
+        assert len(clipped.points) == 2
+        assert np.array_equal(clipped.point_indexer, [5, 6])
+
+        clipped = self.p.clip_points(distance=0.5)
+        assert isinstance(clipped, pr.Preprocessor)
+        assert len(clipped.points) == 1
+        assert np.array_equal(clipped.point_indexer, [6])
+
+    def test_interpolate_lines_to_points(self):
+        with pytest.raises(ValueError, match="If values_as_distance is False"):
+            self.p.interpolate_lines_to_points(distance=None, values_as_distance=False)
+
+        interpolated = self.p.interpolate_lines_to_points(distance=1.0)
+        assert isinstance(interpolated, pr.Preprocessor)
+        assert len(interpolated.points) == 27
+        assert np.array_equal(
+            interpolated.point_indexer, [5, 6, 7] + [3] * 12 + [4] * 12
+        )
+
+        interpolated = self.vp.interpolate_lines_to_points(values_as_distance=True)
+        assert isinstance(interpolated, pr.Preprocessor)
+        assert len(interpolated.points) == 49
+        assert np.array_equal(
+            interpolated.point_indexer, [1, 1, 1] + [0] * 23 + [0] * 23
+        )
+
+    def test_snap_points(self):
+        snapped = self.p.snap_points(0.05)
+        assert isinstance(snapped, pr.Preprocessor)
+        assert len(snapped.points) == 3
+        assert np.array_equal(snapped.point_indexer, [5, 6, 7])
+
+        snapped = self.p.snap_points(2.0)
+        assert isinstance(snapped, pr.Preprocessor)
+        assert len(snapped.points) == 2
+        assert np.array_equal(snapped.point_indexer, [5, 7])
+
+    def test_empty_ops(self):
+        p = pr.Preprocessor(geometry=[outer])
+        assert isinstance(p.clip_points(), pr.Preprocessor)
+        assert isinstance(p.snap_points(1.0), pr.Preprocessor)
+        assert isinstance(p.clip_lines(), pr.Preprocessor)
+        assert isinstance(p.unify_lines(), pr.Preprocessor)
+        assert isinstance(p.interpolate_lines_to_points(), pr.Preprocessor)
+        assert isinstance(p.unify_polygons(), pr.Preprocessor)
+        assert isinstance(p.merge_polygons(), pr.Preprocessor)
+        assert isinstance(p.to_geodataframe(), gpd.GeoDataFrame)
diff --git a/tests/test_snapping.py b/tests/test_snapping.py
new file mode 100644
index 0000000..5ef3cf3
--- /dev/null
+++ b/tests/test_snapping.py
@@ -0,0 +1,71 @@
+import os
+
+import numpy as np
+import pytest
+from scipy import sparse
+
+from pandamesh.snapping import MatrixCSR, columns_and_values, row_slice, snap_nodes
+
+
+def numba_enabled() -> bool:
+    return os.environ.get("NUMBA_DISABLE_JIT") != "1"
+
+
+@pytest.fixture(scope="function")
+def csr_matrix():
+    i = np.repeat(np.arange(5), 2)
+    j = np.arange(10)
+    weights = np.full(10, 0.5)
+    A = sparse.csr_matrix((weights, (i, j)))
+    return MatrixCSR.from_csr_matrix(A)
+
+
+@pytest.mark.skipif(
+    numba_enabled(),
+    reason="Function returns a slice object; python and no-python slices don't mix.",
+)
+def test_row_slice(csr_matrix):
+    # These functions work fine if called inside of other numba functions when
+    # numba is enabled.
+    assert row_slice(csr_matrix, 0) == slice(0, 2, None)
+
+
+@pytest.mark.skipif(
+    numba_enabled(),
+    reason="Function returns a zip object; python and no-python zips don't mix.",
+)
+def test_columns_and_values(csr_matrix):
+    # These functions work fine if called inside of other numba functions when
+    # numba is enabled.
+    zipped = columns_and_values(csr_matrix, row_slice(csr_matrix, 0))
+    result = list(zipped)
+    assert result == [(0, 0.5), (1, 0.5)]
+
+
+def test_snap__three_points_horizontal():
+    x = np.array([0.0, 1.0, 2.0])
+    y = np.zeros_like(x)
+    xy = np.column_stack((x, y))
+    index = snap_nodes(xy, 0.1)
+    assert np.array_equal(index, [0, 1, 2])
+
+    index = snap_nodes(xy, 1.0)
+    assert np.array_equal(index, [0, 2])
+
+    index = snap_nodes(xy, 2.0)
+    assert np.array_equal(index, [0])
+
+
+def test_snap__three_points_diagonal():
+    x = y = np.array([0.0, 1.0, 1.5])
+    xy = np.column_stack((x, y))
+    index = snap_nodes(xy, 0.1)
+    assert np.array_equal(index, [0, 1, 2])
+
+    # hypot(0.5, 0.5) = 0.707...
+    index = snap_nodes(xy, 0.71)
+    assert np.array_equal(index, [0, 1])
+
+    # hypot(1, 1) = 1.414...
+    index = snap_nodes(xy, 1.42)
+    assert np.array_equal(index, [0, 2])
diff --git a/tests/test_triangle_geometry.py b/tests/test_triangle_geometry.py
index 25c8117..108616b 100644
--- a/tests/test_triangle_geometry.py
+++ b/tests/test_triangle_geometry.py
@@ -89,7 +89,7 @@ def test_add_polygons():
     assert np.allclose(vertices, expected)
     assert np.array_equal(segments, expected_segments)
     assert regions[0, 2] == 0
-    assert regions[0, 3] == 0.5 * cellsize**2
+    assert np.allclose(regions[0, 3], 0.25 * np.sqrt(3) * cellsize**2)
     assert sg.Point(x, y).within(donut)