Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zonal mean helpers Fix #955

Merged
merged 27 commits into from
Sep 17, 2024

Conversation

hongyuchen1030
Copy link
Contributor

@hongyuchen1030 hongyuchen1030 commented Sep 14, 2024

Overview

  • This PR introduces bug fixes related to the Non-Conservative Zonal Mean Non-Conservative Zonal Mean #785 issue.
  • Addresses conflicts caused by optimizations in functions merged into main.
  • Fixes bugs in the following algorithms:
    • _get_zonal_faces_weight_at_constLat
    • gca_constlat_intersection
    • _pole_point_inside_polygon
  • Updates related to the point_within_gca function used in the Zonal Mean PR.

Expected Usage

import uxarray as ux

grid_path = "/path/to/grid.nc"
data_path = "/path/to/data.nc"

uxds = ux.open_dataset(grid_path, data_path)

# Example usage of the updated functions
zonal_faces_weight = uxds._get_zonal_faces_weight_at_constLat()
intersection = uxds.gca_constlat_intersection()
polygon_check = uxds._pole_point_inside_polygon()

PR Checklist

General

  • An issue is linked created and linked
  • Add appropriate labels
  • Filled out Overview and Expected Usage (if applicable) sections

Testing

  • Adequate tests are created if there is new functionality
  • Tests cover all possible logical paths in your function
  • Tests are not too basic (such as simply calling a function and nothing else)

Documentation

  • Docstrings have been added to all new functions
  • Docstrings have updated with any function changes
  • Internal functions have a preceding underscore (_) and have been added to docs/internal_api/index.rst
  • User functions have been added to docs/user_api/index.rst

Examples

  • Any new notebook examples added to docs/examples/ folder
  • Clear the output of all cells before committing
  • New notebook files added to docs/examples.rst toctree
  • New notebook files added to new entry in docs/gallery.yml with appropriate thumbnail photo in docs/_static/thumbnails/

@hongyuchen1030 hongyuchen1030 added bug Something isn't working improvement Improvements on existing features or infrastructure labels Sep 14, 2024
@philipc2 philipc2 added the run-benchmark Run ASV benchmark workflow label Sep 15, 2024
Copy link
Collaborator

@amberchen122 amberchen122 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!

@philipc2
Copy link
Member

philipc2 commented Sep 15, 2024

Hi @hongyuchen1030

The ASV Benchmarking bot isn't posting the result here, however from the CI run above we are getting the following faluires:

Change Before [c66286b] After [14bebc6] Ratio Benchmark (Parameter)
! 380M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
! 381M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
! 386M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
! 384M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
! 1.41±0s failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
! 182±0.6ms failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
! 1.68±0s failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
! 8.03±0.04ms failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
+ 370M 421M 1.14 mpas_ocean.Integrate.peakmem_integrate('120km')

May you investigate the grids that are failing?

  • test/meshfiles/mpas/QU/oQU480.231010.nc
  • test/meshfiles/scrip/outCSne8/outCSne8.nc
  • test/meshfiles/ugrid/geoflow-small/grid.nc
  • test/meshfiles/ugrid/quad-hexagon/grid.nc'

@philipc2 philipc2 added run-benchmark Run ASV benchmark workflow and removed run-benchmark Run ASV benchmark workflow labels Sep 15, 2024
@hongyuchen1030
Copy link
Contributor Author

Hi @hongyuchen1030

The ASV Benchmarking bot isn't posting the result here, however from the CI run above we are getting the following faluires:

Change Before [c66286b] After [14bebc6] Ratio Benchmark (Parameter)
! 380M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
! 381M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
! 386M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
! 384M failed n/a face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
! 1.41±0s failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
! 182±0.6ms failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
! 1.68±0s failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
! 8.03±0.04ms failed n/a face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))

  • 370M 421M 1.14 mpas_ocean.Integrate.peakmem_integrate('120km')
    May you investigate the grids that are failing?
  • test/meshfiles/mpas/QU/oQU480.231010.nc
  • test/meshfiles/scrip/outCSne8/outCSne8.nc
  • test/meshfiles/ugrid/geoflow-small/grid.nc
  • test/meshfiles/ugrid/quad-hexagon/grid.nc'

It looks like the zonal_mean branch also update the face-bounds function. I am looking into it right now.

@hongyuchen1030
Copy link
Contributor Author

hongyuchen1030 commented Sep 15, 2024

@philipc2

I’ve pinpointed where the issue occurs, and it’s once again related to the unnormalized coordinates in MPAS. Specifically, it's connected to the file grid_mpas = current_path / "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc as mentioned in #156.

In the extreme_gca_latitude function, the current implementation in main has been modified to the following somehow:

def extreme_gca_latitude(gca_cart, extreme_type):
    extreme_type = extreme_type.lower()

    if extreme_type not in ("max", "min"):
        raise ValueError("extreme_type must be either 'max' or 'min'")

    n1, n2 = gca_cart
    dot_n1_n2 = np.dot(n1, n2)
    denom = (n1[2] + n2[2]) * (dot_n1_n2 - 1.0)
    d_a_max = (n1[2] * dot_n1_n2 - n2[2]) / denom

    d_a_max = (
        np.clip(d_a_max, 0, 1)
        if np.isclose(d_a_max, [0, 1], atol=MACHINE_EPSILON).any()
        else d_a_max
    )

    # Before we make sure the grid coordinates are normalized, do not try to skip the normalization steps!
    _, lat_n1 = _xyz_to_lonlat_rad_no_norm(n1[0], n1[1], n1[2])
    _, lat_n2 = _xyz_to_lonlat_rad_no_norm(n2[0], n2[1], n2[2])

    if 0 < d_a_max < 1:
        node3 = (1 - d_a_max) * n1 + d_a_max * n2
        node3 = np.array(_normalize_xyz_scalar(node3[0], node3[1], node3[2]))
        d_lat_rad = np.arcsin(np.clip(node3[2], -1, 1))

        return (
            max(d_lat_rad, lat_n1, lat_n2)
            if extreme_type == "max"
            else min(d_lat_rad, lat_n1, lat_n2)
        )
    else:
        return max(lat_n1, lat_n2) if extreme_type == "max" else min(lat_n1, lat_n2)

The latest changes are highly risky if we haven’t ensured that the input grid coordinates are normalized uniformly:

    _, lat_n1 = _xyz_to_lonlat_rad_no_norm(n1[0], n1[1], n1[2])
    _, lat_n2 = _xyz_to_lonlat_rad_no_norm(n2[0], n2[1], n2[2])

Because if n1 and n2 are not normalized, the returned longitude won’t be in the range of [0,360], and the latitude won’t be within [0,90] or [0,-90] degrees, which will lead to the corruption of all subsequent functions.

This makes it critical to be extremely cautious when using _xyz_to_lonlat_rad_no_norm. We should avoid using this function altogether if we’re not completely certain that the input is normalized.

@hongyuchen1030
Copy link
Contributor Author

Now, I have added the following test in test_geometry and they all passed (They are basically the file used in benchmark). I don't know why they are still failing on the benchmark though. @philipc2 I wonder if you have any ideas about it? thanks

gridfile_CSne8 = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc"
grid_quad_hex = current_path/ "meshfiles" / "ugrid" / "quad-hexagon" / "grid.nc"
grid_geoflow = current_path/ "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc"
grid_mpas = current_path/ "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc"

grid_files_latlonBound = [grid_quad_hex, grid_geoflow, gridfile_CSne8, grid_mpas]
class TestLatlonBoundsFiles:

    def test_face_bounds(self):
        """Test to ensure ``Grid.face_bounds`` works correctly for all grid
        files."""
        for grid_path in grid_files_latlonBound:
            try:
                # Open the grid file
                self.uxgrid = ux.open_grid(grid_path)

                # Test: Ensure the bounds are obtained
                bounds = self.uxgrid.bounds
                assert bounds is not None, f"Grid.face_bounds should not be None for {grid_path}"

            except Exception as e:
                # Print the failing grid file and re-raise the exception
                print(f"Test failed for grid file: {grid_path}")
                raise e

            finally:
                # Clean up the grid object
                del self.uxgrid

Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made a change to asv.conf.json that should fix the failing tests (they were due to the following)

nschloe/pyfma#17

uxarray/grid/intersections.py Outdated Show resolved Hide resolved
@philipc2
Copy link
Member

Benchmarks that have got worse:

Change Before [c66286b] After [3ccf983] Ratio Benchmark (Parameter)
+ 171±0.9ms 730±9ms 4.28 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
+ 1.63±0s 6.92±0.01s 4.24 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
+ 7.87±0.2ms 20.5±0.2ms 2.6 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))

I will profile this current implementation and see what we need to adjust tommorow.

Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the root cause of the decrease in performance was that most of the existing calls to our numba wrapped numpy functions were removed.

Please replace any the following functions with their respective ones from uxarray.utils.computing

  • np.all
  • np.isclose
  • np.allclose
  • np.cross
  • np.dot

@njit
def all(a):
"""Numba decorated implementation of ``np.all()``
See Also
--------
numpy.all
"""
return np.all(a)
@njit
def isclose(a, b, rtol=1e-05, atol=1e-08):
"""Numba decorated implementation of ``np.isclose()``
See Also
--------
numpy.isclose
"""
return np.isclose(a, b, rtol=rtol, atol=atol)
@njit
def allclose(a, b, rtol=1e-05, atol=1e-08):
"""Numba decorated implementation of ``np.allclose()``
See Also
--------
numpy.allclose
"""
return np.allclose(a, b, rtol=rtol, atol=atol)
@njit
def cross(a, b):
"""Numba decorated implementation of ``np.cross()``
See Also
--------
numpy.cross
"""
return np.cross(a, b)
@njit
def dot(a, b):
"""Numba decorated implementation of ``np.dot()``
See Also
--------
numpy.dot
"""
return np.dot(a, b)

@hongyuchen1030
Copy link
Contributor Author

I believe the root cause of the decrease in performance was that most of the existing calls to our numba wrapped numpy functions were removed.

Please replace any the following functions with their respective ones from uxarray.utils.computing

  • np.all
  • np.isclose
  • np.allclose
  • np.cross
  • np.dot

@njit
def all(a):
"""Numba decorated implementation of ``np.all()``
See Also
--------
numpy.all
"""
return np.all(a)
@njit
def isclose(a, b, rtol=1e-05, atol=1e-08):
"""Numba decorated implementation of ``np.isclose()``
See Also
--------
numpy.isclose
"""
return np.isclose(a, b, rtol=rtol, atol=atol)
@njit
def allclose(a, b, rtol=1e-05, atol=1e-08):
"""Numba decorated implementation of ``np.allclose()``
See Also
--------
numpy.allclose
"""
return np.allclose(a, b, rtol=rtol, atol=atol)
@njit
def cross(a, b):
"""Numba decorated implementation of ``np.cross()``
See Also
--------
numpy.cross
"""
return np.cross(a, b)
@njit
def dot(a, b):
"""Numba decorated implementation of ``np.dot()``
See Also
--------
numpy.dot
"""
return np.dot(a, b)

I have updated the current implementation with their respective ones from uxarray.utils.computing.

I did found some leftover calls in the main branch, but we can fix them after we merge the zonal mean

@philipc2
Copy link
Member

Looks like there is still about a 2.5x decrease in performance.

Benchmarks that have improved:

Change Before [c66286b] After [906f0fc] Ratio Benchmark (Parameter)
- 406M 354M 0.87 mpas_ocean.Integrate.peakmem_integrate('480km')

Benchmarks that have stayed the same:

Change Before [c66286b] After [906f0fc] Ratio Benchmark (Parameter)
381M 383M 1.01 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
381M 384M 1.01 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
386M 387M 1.00 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
384M 385M 1.00 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
1.36±0.01s 3.11±0.01s ~2.29 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
1.67±0.01s 4.22±0.03s ~2.53 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
1.76±0.02s 1.75±0.01s 1.00 import.Imports.timeraw_import_uxarray
641±3ms 643±2ms 1.00 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
40.9±0.5ms 40.5±0.5ms 0.99 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
492±3μs 486±7μs 0.99 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
1.29±0.01μs 1.34±0μs 1.03 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
313±3ns 302±2ns 0.97 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
793±3ns 807±6ns 1.02 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
289±5ns 285±2ns 0.98 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
402M 402M 1.00 mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('120km', False)
390M 391M 1.00 mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('120km', True)
362M 362M 1.00 mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('480km', False)
361M 361M 1.00 mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('480km', True)
1.03±0s 1.03±0s 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
57.7±0.6ms 58.0±1ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
77.7±0.9ms 77.4±0.3ms 1.00 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.64±0.1ms 5.54±0.04ms 0.98 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
263M 263M 1.00 mpas_ocean.Gradient.peakmem_gradient('120km')
241M 241M 1.00 mpas_ocean.Gradient.peakmem_gradient('480km')
2.72±0.01ms 2.70±0.02ms 0.99 mpas_ocean.Gradient.time_gradient('120km')
289±1μs 283±0.9μs 0.98 mpas_ocean.Gradient.time_gradient('480km')
239±20μs 249±5μs 1.04 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
121±1μs 121±3μs 1.00 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
370M 370M 1.00 mpas_ocean.Integrate.peakmem_integrate('120km')
175±1ms 177±0.8ms 1.01 mpas_ocean.Integrate.time_integrate('120km')
12.1±0.2ms 11.9±0.4ms 0.98 mpas_ocean.Integrate.time_integrate('480km')
353±1ms 349±2ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
351±1ms 346±1ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
353±3ms 347±0.9ms 0.98 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
23.2±0.2ms 22.7±0.2ms 0.98 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
23.6±0.2ms 22.8±0.1ms 0.97 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
23.5±0.2ms 22.5±0.3ms 0.96 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
54.7±0.09ms 54.1±0.1ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
44.4±0.1ms 43.9±0.1ms 0.99 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
359±0.4ms 358±1ms 1.00 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
264±0.6ms 262±0.9ms 0.99 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
239M 237M 0.99 quad_hexagon.QuadHexagon.peakmem_open_dataset
236M 236M 1.00 quad_hexagon.QuadHexagon.peakmem_open_grid
6.47±0.04ms 6.56±0.07ms 1.01 quad_hexagon.QuadHexagon.time_open_dataset
5.62±0.08ms 5.53±0.05ms 0.98 quad_hexagon.QuadHexagon.time_open_grid

Benchmarks that have got worse:

Change Before [c66286b] After [906f0fc] Ratio Benchmark (Parameter)
+ 179±3ms 448±0.5ms 2.5 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
+ 7.95±0.03ms 12.9±0.1ms 1.62 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
+ 1.69±0.1ms 2.10±0.2ms 1.24 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')

Comment on lines +130 to +134
cond_number = np.linalg.cond(jacobian)
print(f"Condition number: {cond_number}")
print(f"Jacobian matrix:\n{jacobian}")
print(f"An error occurred: {e}")
raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These look like leftover debugging prints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for what happens if an exception occurs. But if you think an user doesn't need this information, you can remove it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I can update this (using prints is typically not advised)

uxarray/grid/integrate.py Outdated Show resolved Hide resolved
uxarray/grid/integrate.py Show resolved Hide resolved
Comment on lines +143 to +152
except ValueError:
print(f"Face index: {face_index}")
print(f"Face edges information: {face_edges}")
print(f"Constant z0: {latitude_cart}")
print(
f"Face latlon bound information: {face_latlon_bound_candidate[face_index]}"
)
print(f"Face interval information: {face_interval_df}")
# Handle the exception or propagate it further if necessary
raise
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover debugging prints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for what happens if an exception occurs. But if you think an user doesn't need this information, you can remove it

uxarray/grid/intersections.py Outdated Show resolved Hide resolved
uxarray/grid/intersections.py Outdated Show resolved Hide resolved
@philipc2
Copy link
Member

Latest benchmark runs. After a few changes, the performance is only slightly worse, which is acceptable and will be ironed out in
#937

image

Will make a few final changes and get this merged today.

Copy link
Member

@philipc2 philipc2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!

@philipc2 philipc2 merged commit 3f8929d into UXARRAY:main Sep 17, 2024
17 of 20 checks passed
@hongyuchen1030
Copy link
Contributor Author

Nice work!

Thanks for your great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working improvement Improvements on existing features or infrastructure run-benchmark Run ASV benchmark workflow
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants