Repository: QuantEcon/QuantEcon.py (python)

Issue:
CI: Add workflow testing against NumPy 2.0
Add an additional workflow to test against `Numpy 2.0`. I don't know how to do it...

Pull request description:
FIX: Fix NumPy v2 compatibility issue
Close #734

There is only one error with `NumPy 2.0`, which is fixed by this PR.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9bad1bf..7450658 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,10 @@ jobs:
     strategy:
       matrix:
         os: [windows-latest, ubuntu-latest, macos-latest]
-        python-version: [3.9, '3.10', 3.11]
+        python-version: ['3.10', 3.11, 3.12]
+        exclude:
+          - os: windows-latest
+            python-version: 3.12
 
     steps:
     - uses: actions/checkout@v4
@@ -52,6 +55,7 @@ jobs:
     - name: Setup QuantEcon
       shell: bash -l {0}
       run: |
+        python -m ensurepip --upgrade
         python -m pip install -U pip
         pip install .[testing]
 
diff --git a/.github/workflows/ci_np2.yml b/.github/workflows/ci_np2.yml
new file mode 100644
index 0000000..c3f7446
--- /dev/null
+++ b/.github/workflows/ci_np2.yml
@@ -0,0 +1,32 @@
+name: conda-build (NumPy v2)
+
+on: [push]
+
+jobs:
+  build-linux:
+    runs-on: ubuntu-latest
+    strategy:
+      max-parallel: 5
+
+    steps:
+    - uses: actions/checkout@v4
+    - name: Set up Python
+      uses: actions/setup-python@v3
+      with:
+        python-version: '3.12'
+    - name: Add conda to system path
+      run: |
+        # $CONDA is an environment variable pointing to the root of the miniconda directory
+        echo $CONDA/bin >> $GITHUB_PATH
+    - name: Install dependencies
+      run: |
+        conda env update --file environment_np2.yml --name base
+    - name: Conda info
+      shell: bash -l {0}
+      run: |
+        conda info
+        conda list
+    - name: Test with pytest
+      run: |
+        conda install pytest
+        pytest
diff --git a/environment.yml b/environment.yml
index 1e757c5..82fbb59 100644
--- a/environment.yml
+++ b/environment.yml
@@ -12,6 +12,7 @@ dependencies:
   - ipython
   - flake8
   - requests
+  - urllib3>=2
   - flit
   - chardet # python>3.9,osx
   - pytest
diff --git a/environment_np2.yml b/environment_np2.yml
new file mode 100644
index 0000000..bcb4827
--- /dev/null
+++ b/environment_np2.yml
@@ -0,0 +1,18 @@
+name: qe
+channels:
+  - conda-forge
+  - defaults
+dependencies:
+  - coverage
+  - numpy>=2
+  - scipy
+  - pandas
+  - numba
+  - sympy
+  - ipython
+  - flake8
+  - requests
+  - urllib3>=2
+  - flit
+  - chardet # python>3.9,osx
+  - pytest
diff --git a/quantecon/markov/gth_solve.py b/quantecon/markov/gth_solve.py
index b3257c7..76ca043 100644
--- a/quantecon/markov/gth_solve.py
+++ b/quantecon/markov/gth_solve.py
@@ -6,6 +6,9 @@ chain by the Grassmann-Taksar-Heyman (GTH) algorithm.
 import numpy as np
 from numba import jit
 
+from ..util.compat import copy_if_needed
+
+
 def gth_solve(A, overwrite=False, use_jit=True):
     r"""
     This routine computes the stationary distribution of an irreducible
@@ -52,7 +55,9 @@ def gth_solve(A, overwrite=False, use_jit=True):
        Simulation, Princeton University Press, 2009.
 
     """
-    A1 = np.array(A, dtype=float, copy=not overwrite, order='C')
+    copy = copy_if_needed if overwrite else True
+
+    A1 = np.array(A, dtype=float, copy=copy, order='C')
     # `order='C'` is for use with Numba <= 0.18.2
     # See issue github.com/numba/numba/issues/1103
 
diff --git a/quantecon/util/compat.py b/quantecon/util/compat.py
new file mode 100644
index 0000000..5037eec
--- /dev/null
+++ b/quantecon/util/compat.py
@@ -0,0 +1,23 @@
+"""
+Utilities for compatibility
+
+"""
+from typing import Optional
+import numpy as np
+
+
+# From scipy/_lib/_util.py
+
+copy_if_needed: Optional[bool]
+
+if np.lib.NumpyVersion(np.__version__) >= "2.0.0":
+    copy_if_needed = None
+elif np.lib.NumpyVersion(np.__version__) < "1.28.0":
+    copy_if_needed = False
+else:
+    # 2.0.0 dev versions, handle cases where copy may or may not exist
+    try:
+        np.array([1]).__array__(copy=None)  # type: ignore[call-overload]
+        copy_if_needed = None
+    except TypeError:
+        copy_if_needed = False


Test patch:
diff --git a/quantecon/markov/tests/test_gth_solve.py b/quantecon/markov/tests/test_gth_solve.py
index f5275e8..16651f7 100644
--- a/quantecon/markov/tests/test_gth_solve.py
+++ b/quantecon/markov/tests/test_gth_solve.py
@@ -196,6 +196,13 @@ def test_matrices_with_C_F_orders():
     assert_array_equal(computed_F, stationary_dist)
 
 
+def test_unable_to_avoid_copy():
+    A = np.array([[0, 1], [0, 1]])  # dtype=int
+    stationary_dist = [0., 1.]
+    x = gth_solve(A, overwrite=True)
+    assert_array_equal(x, stationary_dist)
+
+
 def test_raises_value_error_non_2dim():
     """Test with non 2dim input"""
     assert_raises(ValueError, gth_solve, np.array([0.4, 0.6]))