From 512e7ec0a7d536d66dac37e1dd1cbe097bbb2e7d Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 21:32:54 -0500
Subject: [PATCH 01/20] github ci updates

---
 .github/workflows/main.yml           |  6 +++---
 .github/workflows/python-publish.yml | 26 +++++++++++++-------------
 2 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9e0a57df..91da142d 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -60,9 +60,9 @@ jobs:
             toxenv: build_docs
 
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install testing dependencies
@@ -71,6 +71,6 @@ jobs:
       run: tox -v -e ${{ matrix.toxenv }}
     - name: Upload coverage to codecov
       if: ${{ contains(matrix.toxenv,'-cov') }}
-      uses: codecov/codecov-action@v1.0.13
+      uses: codecov/codecov-action@v3
       with:
         file: ./coverage.xml
\ No newline at end of file
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 4fe8c22b..377f4c0d 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -10,8 +10,8 @@ jobs:
       matrix:
         os: [ubuntu-latest]
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.9'
@@ -25,7 +25,7 @@ jobs:
           CIBW_BUILD: cp37-manylinux_x86_64 cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v3
         with:
           path: ./wheelhouse/*.whl
 
@@ -37,8 +37,8 @@ jobs:
       matrix:
         os: [macos-latest]
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.9'
@@ -52,7 +52,7 @@ jobs:
           CIBW_BUILD: cp37-* cp38-* cp39-* cp310-*
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v3
         with:
           path: ./wheelhouse/*.whl
 
@@ -64,8 +64,8 @@ jobs:
       matrix:
         os: [windows-latest]
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.9'
@@ -79,7 +79,7 @@ jobs:
           CIBW_BUILD: cp37-* cp38-* cp39-*
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v3
         with:
           path: ./wheelhouse/*.whl
 
@@ -87,8 +87,8 @@ jobs:
     name: Build source distribution
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v2
-      - uses: actions/setup-python@v2
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.9'
@@ -98,7 +98,7 @@ jobs:
           python -m pip install build
       - name: Build sdist
         run: python -m build --sdist --outdir dist/ .
-      - uses: actions/upload-artifact@v2
+      - uses: actions/upload-artifact@v3
         with:
           path: dist/*.tar.gz
 
@@ -108,7 +108,7 @@ jobs:
     runs-on: ubuntu-latest
     if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
     steps:
-      - uses: actions/download-artifact@v2
+      - uses: actions/download-artifact@v3
         with:
           name: artifact
           path: dist

From d0fc13a6576b73265930f245853a0051984df81c Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 21:39:43 -0500
Subject: [PATCH 02/20] Fix for numpy=1.24

---
 turbustat/moments/_moment_errs.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/turbustat/moments/_moment_errs.py b/turbustat/moments/_moment_errs.py
index fcffaeb2..679eb22f 100644
--- a/turbustat/moments/_moment_errs.py
+++ b/turbustat/moments/_moment_errs.py
@@ -238,7 +238,7 @@ def _slice0(cube, axis, scale):
     result = np.zeros(shp) * cube.unit ** 2
 
     view = [slice(None)] * 3
-    valid = np.zeros(shp, dtype=np.bool)
+    valid = np.zeros(shp, dtype=bool)
 
     for i in range(cube.shape[axis]):
         view[axis] = i

From 303ca8327825e20d270c88aa69bb2bff2722343e Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 21:43:08 -0500
Subject: [PATCH 03/20] Ignore warnings

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 52cfa8f3..1586d47d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -48,7 +48,7 @@ testpaths = "turbustat" "docs"
 astropy_header = true
 doctest_plus = enabled
 text_file_format = rst
-addopts = --doctest-rst
+addopts = --doctest-rst -p no:warnings
 
 [coverage:run]
 omit =

From cc46364fed76415f582583f2fe1282a89941a885 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 21:55:57 -0500
Subject: [PATCH 04/20] More fixes for changes in numpy=1.24

---
 turbustat/statistics/pspec_bispec/bispec.py        | 4 ++--
 turbustat/statistics/wavelets/wavelet_transform.py | 2 +-
 turbustat/tests/test_wavelet.py                    | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/turbustat/statistics/pspec_bispec/bispec.py b/turbustat/statistics/pspec_bispec/bispec.py
index 990443bd..b02256ef 100644
--- a/turbustat/statistics/pspec_bispec/bispec.py
+++ b/turbustat/statistics/pspec_bispec/bispec.py
@@ -114,8 +114,8 @@ def compute_bispectrum(self, show_progress=True, use_pyfftw=False,
 
         bispec_shape = (int(self.shape[0] / 2.), int(self.shape[1] / 2.))
 
-        self._bispectrum = np.zeros(bispec_shape, dtype=np.complex)
-        self._bicoherence = np.zeros(bispec_shape, dtype=np.float)
+        self._bispectrum = np.zeros(bispec_shape, dtype=complex)
+        self._bicoherence = np.zeros(bispec_shape, dtype=float)
         self._tracker = np.zeros(self.shape, dtype=np.int16)
 
         biconorm = np.ones_like(self.bispectrum, dtype=float)
diff --git a/turbustat/statistics/wavelets/wavelet_transform.py b/turbustat/statistics/wavelets/wavelet_transform.py
index 9efb3826..b518e702 100644
--- a/turbustat/statistics/wavelets/wavelet_transform.py
+++ b/turbustat/statistics/wavelets/wavelet_transform.py
@@ -152,7 +152,7 @@ def compute_transform(self, show_progress=True, scale_normalization=True,
         A = len(self.scales)
 
         if keep_convolved_arrays:
-            self._Wf = np.zeros((A, n0, m0), dtype=np.float)
+            self._Wf = np.zeros((A, n0, m0), dtype=float)
         else:
             self._Wf = None
 
diff --git a/turbustat/tests/test_wavelet.py b/turbustat/tests/test_wavelet.py
index 6b11c021..d5bc39da 100644
--- a/turbustat/tests/test_wavelet.py
+++ b/turbustat/tests/test_wavelet.py
@@ -67,7 +67,7 @@ def test_Wavelet_method_failbreak():
 
     # No break and only 1 slope
     assert tester.brk is None
-    assert isinstance(tester.slope, np.float)
+    assert isinstance(tester.slope, float)
 
 
 def test_Wavelet_method_fitlimits():

From f28b20622bfb3e0cf9c8f0a2ec976a131c8db931 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 22:07:49 -0500
Subject: [PATCH 05/20] Temporarily use dev version of spectral-cube for
 testing

---
 setup.cfg | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 1586d47d..d8cd5bcc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,7 +21,8 @@ install_requires =
     scikit-learn>=0.13
     statsmodels>=0.4.0
     scikit-image>=0.12
-    spectral_cube
+    # spectral_cube  # Temporarily remove pip version until tag with numpy 1.24 is pushed
+    pip+https://github.com/radio-astro-tools/spectral-cube.git
 
 [extension-helpers]
 use_extension_helpers = true

From 1255c6f75114785d887ed1d37321438cb0273e6b Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 22:15:14 -0500
Subject: [PATCH 06/20] Wrong syntax

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index d8cd5bcc..b617aa41 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,7 +22,7 @@ install_requires =
     statsmodels>=0.4.0
     scikit-image>=0.12
     # spectral_cube  # Temporarily remove pip version until tag with numpy 1.24 is pushed
-    pip+https://github.com/radio-astro-tools/spectral-cube.git
+    git+https://github.com/radio-astro-tools/radio-beam#egg=radio-beam
 
 [extension-helpers]
 use_extension_helpers = true

From 2279a6d1632a4e60f18bdeafad759d78b3b09c77 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Sat, 7 Jan 2023 22:18:37 -0500
Subject: [PATCH 07/20] Revert: this doesn't work

---
 setup.cfg | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index b617aa41..1586d47d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -21,8 +21,7 @@ install_requires =
     scikit-learn>=0.13
     statsmodels>=0.4.0
     scikit-image>=0.12
-    # spectral_cube  # Temporarily remove pip version until tag with numpy 1.24 is pushed
-    git+https://github.com/radio-astro-tools/radio-beam#egg=radio-beam
+    spectral_cube
 
 [extension-helpers]
 use_extension_helpers = true

From 37d185f3431dd31e4b7cf0b1f72e8b3b2db3eb55 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 13:44:16 -0400
Subject: [PATCH 08/20] Non-working version of analytic deriv for
 LogEllipticalPowerLaw2D

---
 turbustat/statistics/elliptical_powerlaw.py | 56 ++++++++++++++++++---
 1 file changed, 50 insertions(+), 6 deletions(-)

diff --git a/turbustat/statistics/elliptical_powerlaw.py b/turbustat/statistics/elliptical_powerlaw.py
index fe72dc90..0779b031 100644
--- a/turbustat/statistics/elliptical_powerlaw.py
+++ b/turbustat/statistics/elliptical_powerlaw.py
@@ -5,6 +5,8 @@
 from astropy.modeling import Fittable2DModel, Parameter, fitting
 from warnings import warn
 
+log_ten = 2.302585092994046
+
 
 def fit_elliptical_powerlaw(values, x, y, p0, fit_method='LevMarq',
                             bootstrap=False, niters=100, alpha=0.6827,
@@ -99,7 +101,8 @@ def fit_elliptical_powerlaw(values, x, y, p0, fit_method='LevMarq',
             model.theta.fixed = True
 
         fitter = fitting.LevMarLSQFitter()
-        fit_model = fitter(model, x, y, values, weights=weights)
+        fit_model = fitter(model, x, y, values, weights=weights,
+                           estimate_jacobian=True)
 
         resids = np.sum(np.abs(values - fit_model(x, y)))
 
@@ -111,7 +114,8 @@ def fit_elliptical_powerlaw(values, x, y, p0, fit_method='LevMarq',
             model_f = LogEllipticalPowerLaw2D(*p0_f)
 
             fitter_f = fitting.LevMarLSQFitter()
-            fit_model_f = fitter(model_f, x, y, values, weights=weights)
+            fit_model_f = fitter(model_f, x, y, values, weights=weights,
+                                 estimate_jacobian=True)
 
             resids_f = np.sum(np.abs(values - fit_model_f(x, y)))
 
@@ -225,10 +229,10 @@ class LogEllipticalPowerLaw2D(Fittable2DModel):
         Power-law index.
     """
 
-    logamplitude = Parameter(default=0, bounds=(-10, None))
-    ellip_transf = Parameter(default=1)
-    theta = Parameter(default=0)
-    gamma = Parameter(default=-1)
+    logamplitude = Parameter(default=0., bounds=(-10, None))
+    ellip_transf = Parameter(default=1, bounds=(-50, 50))
+    theta = Parameter(default=0, bounds=(-np.pi, np.pi))
+    gamma = Parameter(default=-1, bounds=(-20., 20.))
 
     @classmethod
     def evaluate(cls, x, y, logamplitude, ellip_transf, theta, gamma):
@@ -255,6 +259,46 @@ def evaluate(cls, x, y, logamplitude, ellip_transf, theta, gamma):
         return model
 
 
+    # Attempt at numerical implementation for the gradient.
+    # Implementation is not currently stable.
+
+    # def fit_deriv(self, x, y, logamplitude, ellip_transf, theta, gamma):
+    #     """
+    #     Derivatives of the model with respect to parameters
+    #     """
+
+    #     ellip = 1.0 / (1 + np.exp(-ellip_transf))
+
+    #     costhet = np.cos(theta)
+    #     sinthet = np.sin(theta)
+
+    #     q = ellip
+
+    #     term1 = (q * costhet)**2 + sinthet**2
+    #     term2 = 2 * (1 - q**2) * sinthet * costhet
+    #     term3 = (q * sinthet)**2 + costhet**2
+
+    #     r2 = x**2 * term1 + x * y * term2 + y**2 * term3
+
+    #     # print(ellip, costhet, sinthet)
+    #     # print(r2)
+
+    #     d_logamplitude = 0.5 * gamma / (log_ten * r2)
+
+    #     d_ellip_transf = 0.5 * gamma * ellip * (1 - ellip) * (q**2 - 1) * (x**2 * costhet**2 - y**2 * sinthet**2) / r2
+
+    #     d_theta = 0.5 * gamma * ellip * (1 - ellip) * (x**2 - y**2) * sinthet * costhet / r2
+
+    #     d_gamma = 0.5 * np.log10(r2)
+
+    #     # print(d_logamplitude)
+    #     # print(d_ellip_transf)
+    #     # print(d_theta)
+    #     # print(d_gamma)
+
+    #     return [d_logamplitude, d_ellip_transf, d_theta, d_gamma]
+
+
 def interval_transform(x, a, b):
 
     return np.log(x - a) - np.log(b - x)

From cd7e040a5b50f3d146c167e22b9b2b9e2075b77b Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 15:09:25 -0400
Subject: [PATCH 09/20] Add option to fit unbinned data in 1D

---
 turbustat/statistics/base_pspec2.py | 82 +++++++++++++++++++++--------
 1 file changed, 61 insertions(+), 21 deletions(-)

diff --git a/turbustat/statistics/base_pspec2.py b/turbustat/statistics/base_pspec2.py
index 2a2d3482..895c0262 100644
--- a/turbustat/statistics/base_pspec2.py
+++ b/turbustat/statistics/base_pspec2.py
@@ -106,7 +106,8 @@ def compute_radial_pspec(self, logspacing=False, max_bin=None, **kwargs):
         # Attach units to freqs
         self._freqs = self.freqs / u.pix
 
-    def fit_pspec(self, brk=None, log_break=False, low_cut=None,
+    def fit_pspec(self, fit_unbinned=True,
+                  brk=None, log_break=False, low_cut=None,
                   high_cut=None, min_fits_pts=10, weighted_fit=False,
                   bootstrap=False, bootstrap_kwargs={},
                   verbose=False):
@@ -118,6 +119,10 @@ def fit_pspec(self, brk=None, log_break=False, low_cut=None,
 
         Parameters
         ----------
+        fit_unbinned : bool, optional
+            Fits the unbinned 2D power-spectrum to the linear model. Default is True.
+            Use False to fit the binned 1D power-spectrum instead and replicate fitting
+            in earlier TurbuStat versions.
         brk : float or None, optional
             Guesses for the break points. If given as a list, the length of
             the list sets the number of break points to be fit. If a choice is
@@ -150,29 +155,62 @@ def fit_pspec(self, brk=None, log_break=False, low_cut=None,
 
         self._bootstrap_flag = bootstrap
 
-        # Make the data to fit to
-        if low_cut is None:
-            # Default to the largest frequency, since this is just 1 pixel
-            # in the 2D PSpec.
-            self.low_cut = 1. / (0.5 * float(max(self.ps2D.shape)) * u.pix)
-        else:
-            self.low_cut = self._to_pixel_freq(low_cut)
+        if fit_unbinned:
+            yy_freq, xx_freq = make_radial_freq_arrays(self.ps2D.shape)
+
+            freqs_2d = np.sqrt(yy_freq**2 + xx_freq**2) / u.pix
+
+            # Make the data to fit to
+            if low_cut is None:
+                # Default to the largest frequency, since this is just 1 pixel
+                # in the 2D PSpec.
+                self.low_cut = 1. / (0.5 * float(max(self.ps2D.shape)) * u.pix)
+            else:
+                self.low_cut = self._to_pixel_freq(low_cut)
+
+            if high_cut is None:
+                # self.high_cut = self.freqs.max().value / u.pix
+                self.high_cut = freqs_2d.value.max() / u.pix
+            else:
+                self.high_cut = self._to_pixel_freq(high_cut)
+
+
+            clip_mask = clip_func(freqs_2d.value,
+                                self.low_cut.value,
+                                self.high_cut.value)
+
+
+            x = np.log10(freqs_2d.value[clip_mask])
+            y = np.log10(self.ps2D[clip_mask])
 
-        if high_cut is None:
-            self.high_cut = self.freqs.max().value / u.pix
         else:
-            self.high_cut = self._to_pixel_freq(high_cut)
+            # Make the data to fit to
+            if low_cut is None:
+                # Default to the largest frequency, since this is just 1 pixel
+                # in the 2D PSpec.
+                self.low_cut = 1. / (0.5 * float(max(self.ps2D.shape)) * u.pix)
+            else:
+                self.low_cut = self._to_pixel_freq(low_cut)
+
+            if high_cut is None:
+                self.high_cut = self.freqs.max().value / u.pix
+            else:
+                self.high_cut = self._to_pixel_freq(high_cut)
 
-        x = np.log10(self.freqs[clip_func(self.freqs.value, self.low_cut.value,
-                                          self.high_cut.value)].value)
+            x = np.log10(self.freqs[clip_func(self.freqs.value, self.low_cut.value,
+                                            self.high_cut.value)].value)
 
-        clipped_ps1D = self.ps1D[clip_func(self.freqs.value,
-                                           self.low_cut.value,
-                                           self.high_cut.value)]
-        y = np.log10(clipped_ps1D)
+            clipped_ps1D = self.ps1D[clip_func(self.freqs.value,
+                                            self.low_cut.value,
+                                            self.high_cut.value)]
+            y = np.log10(clipped_ps1D)
 
         if weighted_fit:
+            if fit_unbinned:
+                raise NotImplementedError("Error propagation for the unbinned modeling is not "
+                                          "implemented yet.")
 
+            # Currently this will run only for the binned fitting.
             clipped_stddev = self.ps1D_stddev[clip_func(self.freqs.value,
                                                         self.low_cut.value,
                                                         self.high_cut.value)]
@@ -246,7 +284,7 @@ def fit_pspec(self, brk=None, log_break=False, low_cut=None,
             x = sm.add_constant(x)
 
             if weighted_fit:
-                model = sm.WLS(y, x, missing='drop', weights=1 / y_err**2)
+                model = sm.WLS(y, x, missing='drop', weights=weights)
             else:
                 model = sm.OLS(y, x, missing='drop')
 
@@ -614,8 +652,6 @@ def plot_fit(self, show_2D=False, show_residual=True,
         good_interval = clip_func(self.freqs.value, self.low_cut.value,
                                   self.high_cut.value)
 
-        y_fit = self.fit.fittedvalues
-
         if show_residual:
             if isinstance(self.slope, np.ndarray):
                 # Broken linear model
@@ -679,7 +715,11 @@ def plot_fit(self, show_2D=False, show_residual=True,
                                    fmt=symbol, markersize=5, alpha=0.5,
                                    capsize=10, elinewidth=3)
 
-        ax_1D.plot(np.log10(xvals[fit_index]), y_fit, linestyle='-',
+        xvals_plot = np.log10(xvals[fit_index]).ravel()
+        # y_fit = self.fit.fittedvalues
+        y_fit = self.fit.predict(sm.add_constant(xvals_plot))
+
+        ax_1D.plot(xvals_plot, y_fit, linestyle='-',
                    label=label, linewidth=3, color=fit_color)
 
         if show_residual:

From 63a2f531eb94f431e6ba0d93e6d9003cd5439aae Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 15:58:30 -0400
Subject: [PATCH 10/20] Implemented additional LogEllip2DPowerLaw tests

---
 turbustat/tests/test_ellipplaw.py | 88 ++++++++++++++++++++++++++++++-
 1 file changed, 87 insertions(+), 1 deletion(-)

diff --git a/turbustat/tests/test_ellipplaw.py b/turbustat/tests/test_ellipplaw.py
index ac16cdb6..ad1b501b 100644
--- a/turbustat/tests/test_ellipplaw.py
+++ b/turbustat/tests/test_ellipplaw.py
@@ -101,7 +101,7 @@ def test_simple_ellipplaw_2D_anisotropic(plaw, ellip, theta):
     npt.assert_allclose(theta, fit_theta, atol=0.01)
 
 
-@pytest.mark.parametrize('plaw', [2, 3, 4])
+@pytest.mark.parametrize('plaw', [0.5, 1, 1.5, 2, 3, 4, 4.5, 5., 6])
 def test_simple_ellipplaw_2D_isotropic(plaw):
 
     # Must have ellip = 1 for this test. Just be sure...
@@ -176,3 +176,89 @@ def test_simple_ellipplaw_2D_isotropic(plaw):
 
     # And theta should not move
     assert test_stderr[2] == 0.
+
+
+@pytest.mark.parametrize('plaw', [0.5, 1, 1.5, 2, 3, 4, 4.5, 5., 6])
+def test_direct_ellipplaw_2D_isotropic(plaw):
+
+    from astropy.modeling import Fittable2DModel, Parameter, fitting
+
+    # Must have ellip = 1 for this test. Just be sure...
+    # ellip = 1.
+    ellip = 0.99999
+    # ellip = 0.5
+
+    # Theta doesn't matter, but we'll test for what happens when the
+    # elliptical parameters are left free.
+    theta = np.pi / 2.
+
+    imsize = 256
+
+    # Generate a red noise model
+    psd = make_extended(imsize, powerlaw=plaw, ellip=ellip, theta=theta,
+                        return_fft=True)
+
+    psd = np.abs(psd)**2
+
+    # Initial guesses are based on the azimuthally-average spectrum, so it's
+    # valid to give it good initial guesses for the index
+    # Guess it is fairly elliptical. Tends not to be too sensitive to this.
+    ellip_transf = interval_transform(ellip, 0, 1.)
+    # We fit twice w/ thetas offset by pi / 2, so theta also should not be too
+    # sensitive.
+
+    p0 = (3.7,
+          ellip_transf,
+          np.pi / 2.,
+          - (2. + np.random.normal(scale=0.5)))
+
+    yy, xx = np.mgrid[-imsize / 2:imsize / 2, -imsize / 2:imsize / 2]
+
+    # Don't fit the 0, 0 point. It isn't defined by the model.
+    valids = psd != 0.
+
+    assert np.isfinite(psd[valids]).all()
+    assert (psd[valids] > 0.).all()
+
+    model = LogEllipticalPowerLaw2D(*p0)
+
+    # TODO: These are leftover from testing the fit_deriv implementation
+    # there seems to be an instability in how the gradient is calculated.
+    # Return to this in the future.
+
+    # p0_f = list(p0)
+    # p0_f[2] = (p0[2] + np.pi / 2.) % np.pi
+    # model = LogEllipticalPowerLaw2D(*p0_f)
+
+    fitter = fitting.LevMarLSQFitter(calc_uncertainties=True)
+    # fitter = fitting.LevMarLSQFitter(calc_uncertainties=False)
+
+    # fitter = fitting.TRFLSQFitter(calc_uncertainties=True)
+
+    fit_model = fitter(model,
+                       xx[valids],
+                       yy[valids],
+                       np.log10(psd[valids]),
+                       weights=None,
+                       estimate_jacobian=True)
+                    #    estimate_jacobian=False)
+
+    # Do the parameters match?
+
+    params = fit_model.parameters
+
+    # print(f"Init params: {p0}")
+    # print(f"Fit params: {params}")
+    # print(fitter.fit_info)
+
+    # Require the index to be within 0.1 of the actual,
+    # the ellipticity to be within 0.02, and the theta to be within ~3 deg
+
+    npt.assert_allclose(-plaw, params[-1], rtol=0.01, atol=1e-2)
+
+    npt.assert_allclose(1.0, inverse_interval_transform(params[1], 0, 1),
+                        rtol=0.01, atol=1e-2)
+
+    # Theta should be the original
+    assert theta == np.pi / 2.
+

From 7bb038940dabdde8cebd5c32adddede1f1821705 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 17:37:06 -0400
Subject: [PATCH 11/20] Fix weights not being defined

---
 turbustat/statistics/base_pspec2.py | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/turbustat/statistics/base_pspec2.py b/turbustat/statistics/base_pspec2.py
index 895c0262..325a3b0b 100644
--- a/turbustat/statistics/base_pspec2.py
+++ b/turbustat/statistics/base_pspec2.py
@@ -106,7 +106,7 @@ def compute_radial_pspec(self, logspacing=False, max_bin=None, **kwargs):
         # Attach units to freqs
         self._freqs = self.freqs / u.pix
 
-    def fit_pspec(self, fit_unbinned=True,
+    def fit_pspec(self, fit_unbinned=False,
                   brk=None, log_break=False, low_cut=None,
                   high_cut=None, min_fits_pts=10, weighted_fit=False,
                   bootstrap=False, bootstrap_kwargs={},
@@ -219,6 +219,10 @@ def fit_pspec(self, fit_unbinned=True,
 
             y_err = 0.434 * clipped_stddev / clipped_ps1D
 
+            weights = 1 / y_err**2
+        else:
+            weights = None
+
         if brk is not None:
             # Try the fit with a break in it.
             if not log_break:
@@ -230,11 +234,6 @@ def fit_pspec(self, fit_unbinned=True,
                     assert brk.unit == u.dimensionless_unscaled
                     brk = brk.value
 
-            if weighted_fit:
-                weights = 1 / y_err**2
-            else:
-                weights = None
-
             brk_fit = Lm_Seg(x, y, brk, weights=weights)
             brk_fit.fit_model(verbose=verbose, cov_type='HC3')
 

From b54b88e5eecbdbc8acbce0aae3b676cc22125496 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 17:38:18 -0400
Subject: [PATCH 12/20] Ignore notebook checkpoints

---
 .gitignore | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.gitignore b/.gitignore
index e41d8cc6..0d8f4d2f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -64,6 +64,7 @@ docs/api
 *.pyc
 
 *.DS_Store
+*.ipynb_checkpoints
 
 turbustat/version.py
 turbustat/cython_version.py

From 8f37ffa600f58be384a0b71affc68b24b462ac4c Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 17:43:28 -0400
Subject: [PATCH 13/20] Remove pytest-openfiles (see #246)

---
 tox.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tox.ini b/tox.ini
index 41baae71..3ee9fcbd 100644
--- a/tox.ini
+++ b/tox.ini
@@ -32,8 +32,8 @@ extras =
     all: all
 commands =
     pip freeze
-    !cov: pytest --open-files --pyargs turbustat {toxinidir}/docs {posargs}
-    cov: pytest --open-files --pyargs turbustat {toxinidir}/docs --cov turbustat --cov-config={toxinidir}/setup.cfg {posargs}
+    !cov: pytest --pyargs turbustat {toxinidir}/docs {posargs}
+    cov: pytest --pyargs turbustat {toxinidir}/docs --cov turbustat --cov-config={toxinidir}/setup.cfg {posargs}
     cov: coverage xml -o {toxinidir}/coverage.xml
 
 [testenv:build_docs]

From f0b7aa339ee8a3881350da5f5be6f14227df5024 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Thu, 5 Oct 2023 18:15:09 -0400
Subject: [PATCH 14/20] Add binned vs unbinned check for power spectra fits

---
 turbustat/tests/test_pspec.py | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/turbustat/tests/test_pspec.py b/turbustat/tests/test_pspec.py
index f2ed4fdc..c759eea7 100644
--- a/turbustat/tests/test_pspec.py
+++ b/turbustat/tests/test_pspec.py
@@ -187,6 +187,35 @@ def test_pspec_weightfit(plaw):
     npt.assert_allclose(-plaw, test.slope, rtol=0.02)
 
 
+@pytest.mark.parametrize('plaw', np.arange(0.5, 5, 0.67))
+def test_pspec_unbin_vs_bin(plaw):
+    '''
+    The slopes with azimuthal constraints should be the same. When elliptical,
+    the power will be different along the different directions, but the slope
+    should remain the same.
+    '''
+
+    imsize = 64
+    theta = 0
+
+    # Generate a red noise model
+    img = make_extended(imsize, powerlaw=plaw, ellip=1., theta=theta,
+                        return_fft=False)
+
+    test = PowerSpectrum(fits.PrimaryHDU(img))
+    test.run(fit_kwargs={'fit_unbinned': True},
+             fit_2D=False)
+
+    # Ensure slopes are consistent to within 2%
+    npt.assert_allclose(-plaw, test.slope, rtol=0.02)
+
+    test.run(fit_kwargs={'fit_unbinned': False},
+             fit_2D=False)
+
+    # Ensure slopes are consistent to within 2%
+    npt.assert_allclose(-plaw, test.slope, rtol=0.02)
+
+
 @pytest.mark.parametrize('theta',
                          [0., np.pi / 4., np.pi / 2., 7 * np.pi / 8.])
 def test_pspec_fit2D(theta):

From 0ef9ba4a8e6d919cfd103abb2e86098d9768201d Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Fri, 6 Oct 2023 16:27:15 -0400
Subject: [PATCH 15/20] Add kwarg in run for enabling unbinned 1D power
 spectrum fitting

---
 turbustat/statistics/mvc/mvc.py            | 13 +++++++++----
 turbustat/statistics/pspec_bispec/pspec.py | 20 ++++++++++++++------
 turbustat/statistics/vca_vcs/vca.py        | 16 ++++++++++++----
 turbustat/tests/test_pspec.py              |  4 ++--
 4 files changed, 37 insertions(+), 16 deletions(-)

diff --git a/turbustat/statistics/mvc/mvc.py b/turbustat/statistics/mvc/mvc.py
index 0f888f12..d284fef6 100644
--- a/turbustat/statistics/mvc/mvc.py
+++ b/turbustat/statistics/mvc/mvc.py
@@ -229,7 +229,8 @@ def run(self, verbose=False, beam_correct=False,
             use_pyfftw=False, threads=1, pyfftw_kwargs={},
             radial_pspec_kwargs={},
             low_cut=None, high_cut=None,
-            fit_2D=True, fit_kwargs={}, fit_2D_kwargs={},
+            fit_kwargs={}, fit_unbinned=False,
+            fit_2D=True, fit_2D_kwargs={},
             save_name=None, xunit=u.pix**-1, use_wavenumber=False):
         '''
         Full computation of MVC. For fitting parameters and radial binning
@@ -253,10 +254,12 @@ def run(self, verbose=False, beam_correct=False,
             Low frequency cut off in frequencies used in the fitting.
         high_cut : `~astropy.units.Quantity`, optional
             High frequency cut off in frequencies used in the fitting.
-        fit_2D : bool, optional
-            Fit an elliptical power-law model to the 2D power spectrum.
         fit_kwargs : dict, optional
             Passed to `~PowerSpectrum.fit_pspec`.
+        fit_unbinned : bool, optional
+            Passed to `~PowerSpectrum.fit_pspec`. Default is False.
+        fit_2D : bool, optional
+            Fit an elliptical power-law model to the 2D power spectrum.
         fit_2D_kwargs : dict, optional
             Keyword arguments for `~MVC.fit_2Dpspec`. Use the
             `low_cut` and `high_cut` keywords to provide fit limits.
@@ -280,7 +283,9 @@ def run(self, verbose=False, beam_correct=False,
                            **pyfftw_kwargs)
 
         self.compute_radial_pspec(**radial_pspec_kwargs)
-        self.fit_pspec(low_cut=low_cut, high_cut=high_cut, **fit_kwargs)
+        self.fit_pspec(low_cut=low_cut, high_cut=high_cut,
+                       fit_unbinned=fit_unbinned,
+                       **fit_kwargs)
 
         if fit_2D:
             self.fit_2Dpspec(low_cut=low_cut, high_cut=high_cut,
diff --git a/turbustat/statistics/pspec_bispec/pspec.py b/turbustat/statistics/pspec_bispec/pspec.py
index 4cf96a42..bc040aa3 100644
--- a/turbustat/statistics/pspec_bispec/pspec.py
+++ b/turbustat/statistics/pspec_bispec/pspec.py
@@ -122,9 +122,9 @@ def run(self, verbose=False, beam_correct=False,
             apodize_kernel=None, alpha=0.2, beta=0.0,
             use_pyfftw=False, threads=1,
             pyfftw_kwargs={},
-            low_cut=None, high_cut=None,
-            fit_2D=True, radial_pspec_kwargs={}, fit_kwargs={},
-            fit_2D_kwargs={},
+            low_cut=None, high_cut=None, radial_pspec_kwargs={},
+            fit_kwargs={}, fit_unbinned=False,
+            fit_2D=True, fit_2D_kwargs={},
             xunit=u.pix**-1, save_name=None,
             use_wavenumber=False):
         '''
@@ -157,12 +157,14 @@ def run(self, verbose=False, beam_correct=False,
             Low frequency cut off in frequencies used in the fitting.
         high_cut : `~astropy.units.Quantity`, optional
             High frequency cut off in frequencies used in the fitting.
-        fit_2D : bool, optional
-            Fit an elliptical power-law model to the 2D power spectrum.
         radial_pspec_kwargs : dict, optional
             Passed to `~PowerSpectrum.compute_radial_pspec`.
         fit_kwargs : dict, optional
             Passed to `~PowerSpectrum.fit_pspec`.
+        fit_unbinned : bool, optional
+            Passed to `~PowerSpectrum.fit_pspec`. Default is False.
+        fit_2D : bool, optional
+            Fit an elliptical power-law model to the 2D power spectrum.
         fit_2D_kwargs : dict, optional
             Keyword arguments for `PowerSpectrum.fit_2Dpspec`. Use the
             `low_cut` and `high_cut` keywords to provide fit limits.
@@ -178,6 +180,10 @@ def run(self, verbose=False, beam_correct=False,
         if pyfftw_kwargs.get('threads') is not None:
             pyfftw_kwargs.pop('threads')
 
+        # Pop fit_unbinned if given as kwarg in dict.
+        if fit_kwargs.get('fit_unbinned') is not None:
+            fit_kwargs.pop('fit_unbinned')
+
         self.compute_pspec(apodize_kernel=apodize_kernel,
                            alpha=alpha, beta=beta,
                            beam_correct=beam_correct,
@@ -186,7 +192,9 @@ def run(self, verbose=False, beam_correct=False,
 
         self.compute_radial_pspec(**radial_pspec_kwargs)
 
-        self.fit_pspec(low_cut=low_cut, high_cut=high_cut, **fit_kwargs)
+        self.fit_pspec(fit_unbinned=fit_unbinned,
+                       low_cut=low_cut, high_cut=high_cut,
+                       **fit_kwargs)
 
         if fit_2D:
             self.fit_2Dpspec(low_cut=low_cut, high_cut=high_cut,
diff --git a/turbustat/statistics/vca_vcs/vca.py b/turbustat/statistics/vca_vcs/vca.py
index 31b8e429..8baa9d88 100644
--- a/turbustat/statistics/vca_vcs/vca.py
+++ b/turbustat/statistics/vca_vcs/vca.py
@@ -129,7 +129,8 @@ def run(self, verbose=False, beam_correct=False,
             pyfftw_kwargs={},
             radial_pspec_kwargs={},
             low_cut=None, high_cut=None,
-            fit_2D=True, fit_kwargs={}, fit_2D_kwargs={},
+            fit_kwargs={}, fit_unbinned=False,
+            fit_2D=True,  fit_2D_kwargs={},
             save_name=None, xunit=u.pix**-1, use_wavenumber=False):
         '''
         Full computation of VCA.
@@ -163,10 +164,12 @@ def run(self, verbose=False, beam_correct=False,
             Low frequency cut off in frequencies used in the fitting.
         high_cut : `~astropy.units.Quantity`, optional
             High frequency cut off in frequencies used in the fitting.
-        fit_2D : bool, optional
-            Fit an elliptical power-law model to the 2D power spectrum.
         fit_kwargs : dict, optional
             Passed to `~PowerSpectrum.fit_pspec`.
+        fit_unbinned : bool, optional
+            Passed to `~PowerSpectrum.fit_pspec`. Default is False.
+        fit_2D : bool, optional
+            Fit an elliptical power-law model to the 2D power spectrum.
         fit_2D_kwargs : dict, optional
             Keyword arguments for `~VCA.fit_2Dpspec`. Use the
             `low_cut` and `high_cut` keywords to provide fit limits.
@@ -183,6 +186,10 @@ def run(self, verbose=False, beam_correct=False,
         if pyfftw_kwargs.get('threads') is not None:
             pyfftw_kwargs.pop('threads')
 
+        # Pop fit_unbinned if given as kwarg in dict.
+        if fit_kwargs.get('fit_unbinned') is not None:
+            fit_kwargs.pop('fit_unbinned')
+
         self.compute_pspec(apodize_kernel=apodize_kernel,
                            alpha=alpha, beta=beta,
                            beam_correct=beam_correct,
@@ -190,7 +197,8 @@ def run(self, verbose=False, beam_correct=False,
                            **pyfftw_kwargs)
 
         self.compute_radial_pspec(**radial_pspec_kwargs)
-        self.fit_pspec(low_cut=low_cut, high_cut=high_cut, **fit_kwargs)
+        self.fit_pspec(low_cut=low_cut, high_cut=high_cut, fit_unbinned=fit_unbinned,
+                       **fit_kwargs)
 
         if fit_2D:
             self.fit_2Dpspec(low_cut=low_cut, high_cut=high_cut,
diff --git a/turbustat/tests/test_pspec.py b/turbustat/tests/test_pspec.py
index c759eea7..b801c775 100644
--- a/turbustat/tests/test_pspec.py
+++ b/turbustat/tests/test_pspec.py
@@ -203,13 +203,13 @@ def test_pspec_unbin_vs_bin(plaw):
                         return_fft=False)
 
     test = PowerSpectrum(fits.PrimaryHDU(img))
-    test.run(fit_kwargs={'fit_unbinned': True},
+    test.run(fit_unbinned=True,
              fit_2D=False)
 
     # Ensure slopes are consistent to within 2%
     npt.assert_allclose(-plaw, test.slope, rtol=0.02)
 
-    test.run(fit_kwargs={'fit_unbinned': False},
+    test.run(fit_unbinned=False,
              fit_2D=False)
 
     # Ensure slopes are consistent to within 2%

From 9a71dfbd2b6429f00c40d2f1aa851488f187cbde Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Fri, 6 Oct 2023 16:35:36 -0400
Subject: [PATCH 16/20] Handle type error failure

---
 turbustat/statistics/base_pspec2.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/turbustat/statistics/base_pspec2.py b/turbustat/statistics/base_pspec2.py
index 325a3b0b..95eebc6c 100644
--- a/turbustat/statistics/base_pspec2.py
+++ b/turbustat/statistics/base_pspec2.py
@@ -88,7 +88,7 @@ def compute_radial_pspec(self, logspacing=False, max_bin=None, **kwargs):
         '''
 
         # Check if azimuthal constraints are given
-        if kwargs.get("theta_0"):
+        if "theta_0" in kwargs:
             azim_constraint_flag = True
         else:
             azim_constraint_flag = False

From 8740fdaa66cc3c841dbe0adc1e365b16690e1958 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Fri, 6 Oct 2023 17:07:00 -0400
Subject: [PATCH 17/20] Found one more type error case

---
 turbustat/statistics/scf/scf.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/turbustat/statistics/scf/scf.py b/turbustat/statistics/scf/scf.py
index 18ecff47..fdb0cdee 100644
--- a/turbustat/statistics/scf/scf.py
+++ b/turbustat/statistics/scf/scf.py
@@ -258,12 +258,12 @@ def compute_spectrum(self, **kwargs):
         if self.scf_surface is None:
             self.compute_surface()
 
-        if kwargs.get("logspacing"):
+        if "logspacing" in kwargs:
             warn("Disabled log-spaced bins. This does not work well for the"
                  " SCF.", TurbuStatMetricWarning)
             kwargs.pop('logspacing')
 
-        if kwargs.get("theta_0"):
+        if "theta_0" in kwargs:
             azim_constraint_flag = True
         else:
             azim_constraint_flag = False

From e03bd349b0937e0cc892b5e1553fe42b174fab6b Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Fri, 6 Oct 2023 17:22:14 -0400
Subject: [PATCH 18/20] Add minimal py311 testing

---
 .github/workflows/main.yml | 5 +++++
 tox.ini                    | 2 +-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 91da142d..8ec86501 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -14,6 +14,11 @@ jobs:
       fail-fast: false
       matrix:
         include:
+          - os: ubuntu-latest
+            python-version: '3.11'
+            name: Python 3.11 with minimal dependencies
+            toxenv: py311-test
+
           - os: ubuntu-latest
             python-version: '3.10'
             name: Python 3.10 with minimal dependencies
diff --git a/tox.ini b/tox.ini
index 3ee9fcbd..2759c61a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 [tox]
 envlist =
-    py{37,38,39,310}-test{,-all,-dev,-cov}
+    py{37,38,39,310,311}-test{,-all,-dev,-cov}
     build_docs
     codestyle
 requires =

From f69f06138c9dbfa8fb9db42c6f00c005bb6737d4 Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Fri, 6 Oct 2023 19:41:12 -0400
Subject: [PATCH 19/20] Add new versions to packaging action

---
 .github/workflows/python-publish.yml | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 377f4c0d..8e1ccb78 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -14,7 +14,7 @@ jobs:
       - uses: actions/setup-python@v4
         name: Install Python
         with:
-          python-version: '3.9'
+          python-version: '3.11'
       - name: Install cibuildwheel
         run: |
           python -m pip install --upgrade pip
@@ -22,7 +22,7 @@ jobs:
       - name: Build wheels
         run: python -m cibuildwheel --output-dir wheelhouse
         env:
-          CIBW_BUILD: cp37-manylinux_x86_64 cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64
+          CIBW_BUILD: cp37-manylinux_x86_64 cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
       - uses: actions/upload-artifact@v3
@@ -41,7 +41,7 @@ jobs:
       - uses: actions/setup-python@v4
         name: Install Python
         with:
-          python-version: '3.9'
+          python-version: '3.11'
       - name: Install cibuildwheel
         run: |
           python -m pip install --upgrade pip
@@ -49,7 +49,7 @@ jobs:
       - name: Build wheels
         run: python -m cibuildwheel --output-dir wheelhouse
         env:
-          CIBW_BUILD: cp37-* cp38-* cp39-* cp310-*
+          CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-*
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
       - uses: actions/upload-artifact@v3
@@ -68,7 +68,7 @@ jobs:
       - uses: actions/setup-python@v4
         name: Install Python
         with:
-          python-version: '3.9'
+          python-version: '3.11'
       - name: Install cibuildwheel
         run: |
           python -m pip install --upgrade pip
@@ -76,7 +76,7 @@ jobs:
       - name: Build wheels
         run: python -m cibuildwheel --output-dir wheelhouse
         env:
-          CIBW_BUILD: cp37-* cp38-* cp39-*
+          CIBW_BUILD: cp39-* cp310-* cp311-*
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
       - uses: actions/upload-artifact@v3
@@ -91,7 +91,7 @@ jobs:
       - uses: actions/setup-python@v4
         name: Install Python
         with:
-          python-version: '3.9'
+          python-version: '3.11'
       - name: Install build
         run: |
           python -m pip install --upgrade pip
@@ -112,7 +112,7 @@ jobs:
         with:
           name: artifact
           path: dist
-      - uses: pypa/gh-action-pypi-publish@master
+      - uses: pypa/gh-action-pypi-publish@release/v1
         with:
           user: __token__
           password: ${{ secrets.pypi_password }}

From efb85a3953141bfa9ae2d3bb1ec7b3aa02c432fc Mon Sep 17 00:00:00 2001
From: e-koch <koch.eric.w@gmail.com>
Date: Fri, 6 Oct 2023 21:40:06 -0400
Subject: [PATCH 20/20] Update cibuildwheel calls

---
 .github/workflows/python-publish.yml | 28 ++++++++--------------------
 1 file changed, 8 insertions(+), 20 deletions(-)

diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index 8e1ccb78..5efff0bb 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -10,17 +10,13 @@ jobs:
       matrix:
         os: [ubuntu-latest]
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.11'
-      - name: Install cibuildwheel
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install cibuildwheel
       - name: Build wheels
-        run: python -m cibuildwheel --output-dir wheelhouse
+        uses: pypa/cibuildwheel@v2.16.2
         env:
           CIBW_BUILD: cp37-manylinux_x86_64 cp38-manylinux_x86_64 cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64
           CIBW_TEST_EXTRAS: test
@@ -37,17 +33,13 @@ jobs:
       matrix:
         os: [macos-latest]
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.11'
-      - name: Install cibuildwheel
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install cibuildwheel
       - name: Build wheels
-        run: python -m cibuildwheel --output-dir wheelhouse
+        uses: pypa/cibuildwheel@v2.16.2
         env:
           CIBW_BUILD: cp37-* cp38-* cp39-* cp310-* cp311-*
           CIBW_TEST_EXTRAS: test
@@ -64,19 +56,15 @@ jobs:
       matrix:
         os: [windows-latest]
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v4
         name: Install Python
         with:
           python-version: '3.11'
-      - name: Install cibuildwheel
-        run: |
-          python -m pip install --upgrade pip
-          python -m pip install cibuildwheel
       - name: Build wheels
-        run: python -m cibuildwheel --output-dir wheelhouse
+        uses: pypa/cibuildwheel@v2.16.2
         env:
-          CIBW_BUILD: cp39-* cp310-* cp311-*
+          CIBW_BUILD: cp39-*win_amd64 cp310-*win_amd64 cp311-*win_amd64
           CIBW_TEST_EXTRAS: test
           CIBW_TEST_COMMAND: pytest --pyargs turbustat
       - uses: actions/upload-artifact@v3
@@ -87,7 +75,7 @@ jobs:
     name: Build source distribution
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - uses: actions/setup-python@v4
         name: Install Python
         with: