From 449cb1c0d3ec33d0188e1213f5c7cdf1f7a70d45 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 7 Mar 2023 16:54:06 -0500 Subject: [PATCH 01/73] add zenodo metadata --- .zenodo.json | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .zenodo.json diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000..79459fa --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,26 @@ +{ + "license": "MIT", + "creators": [ + { + "orcid": "0000-0003-2174-3308", + "affiliation": "University of California at Berkeley", + "name": "Gregory D. Kahanamoku-Meyer" + }, + { + "orcid": "0000-0002-0512-4139", + "affiliation": "Harvard University", + "name": "Julia Wei" + } + ], + "keywords": [ + "quantum", + "parallel", + "Python", + "Krylov", + "PETSc", + "SLEPc", + "GPU", + "CUDA", + "MPI" + ] +} From 360b96aaabbdd3e573113c99d7c2b4e885978ba7 Mon Sep 17 00:00:00 2001 From: Greg Meyer Date: Thu, 9 Mar 2023 11:24:38 -0800 Subject: [PATCH 02/73] dont look for CUDA compiler in non-CUDA builds --- petsc_config/complex-debug.py | 4 ++++ petsc_config/complex-opt.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/petsc_config/complex-debug.py b/petsc_config/complex-debug.py index 21ce179..6830616 100755 --- a/petsc_config/complex-debug.py +++ b/petsc_config/complex-debug.py @@ -10,6 +10,10 @@ configure_options = [ + # don't try to configure CUDA here + '--with-cuda=0', + '--with-cudac=0', + # use native complex numbers for scalars. currently required for dynamite. '--with-scalar-type=complex', diff --git a/petsc_config/complex-opt.py b/petsc_config/complex-opt.py index 0a41d2e..6d50e7f 100755 --- a/petsc_config/complex-opt.py +++ b/petsc_config/complex-opt.py @@ -10,6 +10,10 @@ configure_options = [ + # don't try to configure CUDA here + '--with-cuda=0', + '--with-cudac=0', + # some optimization flags '--with-debugging=0', From 7ea5778f7239730152dbcdecb9547b032f7d80d1 Mon Sep 17 00:00:00 2001 From: Greg Meyer Date: Fri, 10 Mar 2023 10:44:07 -0800 Subject: [PATCH 03/73] reverse the ordering in Auto when not sorting; fix bug in Explicit --- CHANGELOG.md | 5 +++++ src/dynamite/subspaces.py | 15 ++++++++++++--- tests/integration/test_operators.py | 19 ++++++++++--------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e454c26..3883f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog +## 0.3.2 - IN PROGRESS + +### Fixed + - Explicit subspace sometimes failed conservation check even when operator was actually conserved + ## 0.3.1 - 2023-03-07 ### Fixed diff --git a/src/dynamite/subspaces.py b/src/dynamite/subspaces.py index 83f3a2f..a325497 100644 --- a/src/dynamite/subspaces.py +++ b/src/dynamite/subspaces.py @@ -408,6 +408,12 @@ def __init__(self, state_list, L=None): if np.any(self.rmap_states[1:] == self.rmap_states[:-1]): raise ValueError('values in state_list must be unique') + # need to keep a handle on the contiguous versions of these, + # so they don't get garbage collected + self.state_map = np.ascontiguousarray(self.state_map) + self.rmap_indices = np.ascontiguousarray(self.rmap_indices) + self.rmap_states = np.ascontiguousarray(self.rmap_states) + super().__init__(L=L) def check_L(self, value): @@ -451,9 +457,9 @@ def _get_cdata(self): return bsubspace.CExplicit( self.L, - np.ascontiguousarray(self.state_map), - np.ascontiguousarray(self.rmap_indices), - np.ascontiguousarray(self.rmap_states) + self.state_map, + self.rmap_indices, + self.rmap_states ) @@ -513,6 +519,9 @@ def __init__(self, H, state, size_guess=None, sort=True): if sort: state_map.sort() + else: + # reverse Cuthill-McKee ordering needs... reverse! + state_map = state_map[::-1] Explicit.__init__(self, state_map, L=H.L) diff --git a/tests/integration/test_operators.py b/tests/integration/test_operators.py index c7c7c52..853ef9b 100644 --- a/tests/integration/test_operators.py +++ b/tests/integration/test_operators.py @@ -113,15 +113,16 @@ def test_spinconserve_xparity_error(self): def test_auto(self): for k in (config.L//2, config.L//4): - for H_name in hamiltonians.get_names(complex_enabled()): - if H_name == 'syk' and self.skip_flags['small_only']: - continue - H = getattr(hamiltonians, H_name)() - subspace = Auto(H, 'U'*k + 'D'*(config.L-k)) - with self.subTest(H=H_name, L=config.L, k=k): - self.assertTrue( - H.conserves(subspace) - ) + for sort in (True, False): + for H_name in hamiltonians.get_names(complex_enabled()): + if H_name == 'syk' and self.skip_flags['small_only']: + continue + H = getattr(hamiltonians, H_name)() + subspace = Auto(H, 'U'*k + 'D'*(config.L-k), sort=sort) + with self.subTest(H=H_name, L=config.L, k=k, sort=sort): + self.assertTrue( + H.conserves(subspace) + ) def test_change_parity(self): """ From f8b5f8a0021d2c3c9aec12ed0ace7bfa46aecee6 Mon Sep 17 00:00:00 2001 From: Greg Meyer Date: Fri, 17 Mar 2023 11:33:58 -0700 Subject: [PATCH 04/73] add spin squeezing paper to pubs --- docs/pubs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pubs.md b/docs/pubs.md index e46fd1f..6a8c85b 100644 --- a/docs/pubs.md +++ b/docs/pubs.md @@ -9,3 +9,4 @@ - Schuster *et al.*, “Many-Body Quantum Teleportation via Operator Spreading in the Traversable Wormhole Protocol,” [*Phys. Rev. X*, vol. 12, no. 3, p. 031013, Jul. 2022.](https://doi.org/10.1103/PhysRevX.12.031013) - Schuster *et al.*, “Learning quantum systems via out-of-time-order correlators.” [arXiv, Aug. 03, 2022.](http://arxiv.org/abs/2208.02254) - Schuster *et al.*, “Operator Growth in Open Quantum Systems.” [arXiv, Aug. 25, 2022.](https://doi.org/10.48550/arXiv.2208.12272) + - Bornet *et al.*, “Scalable spin squeezing in a dipolar Rydberg atom array.” [arXiv, Mar. 14 2023.](https://doi.org/10.48550/arXiv.2303.08053) From 28f6b712dffbe2ad3b0df5a7d05e7602831565a3 Mon Sep 17 00:00:00 2001 From: Greg Meyer Date: Mon, 20 Mar 2023 09:43:11 -0700 Subject: [PATCH 05/73] add Operator.expectation() --- CHANGELOG.md | 3 ++ examples/tutorial/2-States.ipynb | 46 ++++++++++++++------- examples/tutorial/3-Eigensolving.ipynb | 55 ++++++++++++++++++------- examples/tutorial/4-TimeEvolution.ipynb | 42 +++++++++++++------ src/dynamite/operators.py | 23 +++++++++++ tests/integration/test_operators.py | 32 ++++++++++++++ 6 files changed, 159 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3883f94..622f7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## 0.3.2 - IN PROGRESS +### Added + - `Operator.expectation()`, convenience function to compute the expectation value of the operator with respect to a state + ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/examples/tutorial/2-States.ipynb b/examples/tutorial/2-States.ipynb index 58b7f56..cfe8d33 100644 --- a/examples/tutorial/2-States.ipynb +++ b/examples/tutorial/2-States.ipynb @@ -16,7 +16,9 @@ "cell_type": "code", "execution_count": null, "id": "83686067-355a-44d3-8702-74aecfc77cd7", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from dynamite.states import State" @@ -36,7 +38,9 @@ "cell_type": "code", "execution_count": null, "id": "ac12aa49-b67e-4a7c-9f61-bb36b70d4690", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# work with a spin chain of size 6 for this whole example\n", @@ -48,7 +52,9 @@ "cell_type": "code", "execution_count": null, "id": "2144c943-45f0-4b9b-b4f4-f04ea7866617", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "s = State(state='UUUUUU') # all up spins\n", @@ -67,7 +73,9 @@ "cell_type": "code", "execution_count": null, "id": "c073971f-ca5b-4291-9353-156fa1fafbd8", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from dynamite.operators import sigmaz" @@ -77,10 +85,12 @@ "cell_type": "code", "execution_count": null, "id": "7d2adcb9-ff5d-4e45-8c62-d0f1ff8588e6", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "s.dot(sigmaz(0)*s) # the complex conjugate of the bra state is implied when calling .dot()" + "sigmaz(0).expectation(s)" ] }, { @@ -97,11 +107,13 @@ "cell_type": "code", "execution_count": null, "id": "f183ec3e-dfa2-4ffb-84ef-e0609ebf1d3b", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "s = State(state='DUUUUU')\n", - "s.dot(sigmaz(0)*s)" + "sigmaz(0).expectation(s)" ] }, { @@ -209,7 +221,9 @@ "cell_type": "code", "execution_count": null, "id": "77db285e-dc98-4666-af41-443de6a56b6d", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "s = State(state='random')\n", @@ -220,7 +234,9 @@ "cell_type": "code", "execution_count": null, "id": "338f79ff-5007-4cb4-bf27-9737788acb37", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "s.norm()" @@ -231,17 +247,19 @@ "id": "8a2a871e-9812-44b1-ac72-c639f35c801d", "metadata": {}, "source": [ - "It has a correspondingly random expectation value (but with zero complex part, since $\\sigma^z$ is Hermitian):" + "It has a correspondingly random expectation value" ] }, { "cell_type": "code", "execution_count": null, "id": "92957c5d-0635-4d29-b583-a64c95a67b6a", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "s.dot(sigmaz(0)*s)" + "sigmaz(0).expectation(s)" ] }, { @@ -271,7 +289,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/examples/tutorial/3-Eigensolving.ipynb b/examples/tutorial/3-Eigensolving.ipynb index 26db78f..2bbb1b4 100644 --- a/examples/tutorial/3-Eigensolving.ipynb +++ b/examples/tutorial/3-Eigensolving.ipynb @@ -26,7 +26,9 @@ "cell_type": "code", "execution_count": null, "id": "939fe3b8-353e-4cf8-b5b0-0cd2a0314743", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# always convenient to globally set the length of the spin chain\n", @@ -38,7 +40,9 @@ "cell_type": "code", "execution_count": null, "id": "f3a207e7", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from dynamite.operators import sigmax, sigmay, sigmaz\n", @@ -62,7 +66,9 @@ "cell_type": "code", "execution_count": null, "id": "6f6f48c5", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "H = tfim(J=1, h=0.1)\n", @@ -85,7 +91,9 @@ "cell_type": "code", "execution_count": null, "id": "a9fc684d-8771-4585-acb5-844a7f31402f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "eigvals = H.eigsolve(nev=3)\n", @@ -104,7 +112,9 @@ "cell_type": "code", "execution_count": null, "id": "26724d22-e87a-400d-abf4-f906f5842fb0", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "eigvals, eigvecs = H.eigsolve(getvecs=True)\n", @@ -123,7 +133,9 @@ "cell_type": "code", "execution_count": null, "id": "fb6ea7cf-6c1d-42db-ad62-b02f8ad41f98", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "gs = eigvecs[0]\n", @@ -134,7 +146,9 @@ "cell_type": "code", "execution_count": null, "id": "77047157-c589-4a55-80ef-cbe0b04f677b", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# Exercise: confirm that the expectation value of the Hamiltonian is equal to the eigenvalue\n" @@ -152,11 +166,13 @@ "cell_type": "code", "execution_count": null, "id": "7bde2806-f063-49bd-885c-d59e72ef3a65", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "def measure_sigmaz(i, state):\n", - " return state.dot(sigmaz(i)*state)\n", + " return sigmaz(i).expectation(state)\n", "\n", "# measurements\n", "z_vals = []\n", @@ -168,7 +184,9 @@ "cell_type": "code", "execution_count": null, "id": "f99e94fa-1609-4357-bf75-99c0dea81018", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", @@ -219,15 +237,16 @@ "cell_type": "code", "execution_count": null, "id": "ef748a83", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# here is a function to measure the total magnetization of a state, to get you started!\n", "\n", "def measure_mtot(state):\n", " mtot = index_sum(sigmaz(0))\n", - " exp_value = state.dot(mtot*state)\n", - " return exp_value.real # only need the real part, it's Hermitian" + " return mtot.expectation(state)" ] }, { @@ -263,7 +282,9 @@ "cell_type": "code", "execution_count": null, "id": "f94f4f66-7707-4607-bbcb-a3d60c802086", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "H = tfim(-1, 0.5) + 1e-6*index_sum(sigmaz())\n", @@ -285,7 +306,9 @@ "cell_type": "code", "execution_count": null, "id": "356c13f7-7706-446e-b7ec-f423145d6daa", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "eigvals, eigvecs = H.eigsolve(getvecs=True) # solve for the ground state\n", @@ -324,7 +347,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/examples/tutorial/4-TimeEvolution.ipynb b/examples/tutorial/4-TimeEvolution.ipynb index f6051a4..027046c 100644 --- a/examples/tutorial/4-TimeEvolution.ipynb +++ b/examples/tutorial/4-TimeEvolution.ipynb @@ -20,7 +20,9 @@ "cell_type": "code", "execution_count": null, "id": "526a51d5-8cc5-43a7-88ac-77cce03d55e5", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from dynamite import config\n", @@ -31,7 +33,9 @@ "cell_type": "code", "execution_count": null, "id": "54ea5bed-544e-4a19-9c2e-92c719316ca4", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from dynamite.operators import sigmax, sigmay, sigmaz, index_sum\n", @@ -58,7 +62,9 @@ "cell_type": "code", "execution_count": null, "id": "5d190fe0-667a-435f-96a3-7e671fbe8cf0", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from dynamite.states import State\n", @@ -72,20 +78,26 @@ "id": "b1e4f9f1-255a-4371-82d3-06b020c8508d", "metadata": {}, "source": [ - "The $\\sigma^z$ expectation values across the chain:" + "The $S^z$ expectation values across the chain:" ] }, { "cell_type": "code", "execution_count": null, "id": "f91bf135-f6b5-4ca3-a92a-959c8875bfc4", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from matplotlib import pyplot as plt\n", "\n", + "# for performance reasons, its a good idea to pre-compute the Sz operators\n", + "# if we're going to be re-using them a lot\n", + "Sz_ops = [0.5*sigmaz(i) for i in range(config.L)]\n", + "\n", "def plot_sigma_z(state):\n", - " z_vals = [state.dot(sigmaz(i)*state).real for i in range(config.L)]\n", + " z_vals = [Szi.expectation(state) for Szi in Sz_ops]\n", " plt.plot(z_vals)\n", " plt.xlabel('Spin index')\n", " plt.ylabel('Z magnetization')\n", @@ -105,7 +117,9 @@ "cell_type": "code", "execution_count": null, "id": "0dd6b5de-c309-4a88-bc2c-a1fcab75113f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "final_state = H.evolve(initial_state, t=3)\n", @@ -132,7 +146,9 @@ "cell_type": "code", "execution_count": null, "id": "d4d1e5e1-6849-4062-a9d1-59ff35844f1e", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "total_evolution_time = 10\n", @@ -141,13 +157,13 @@ "n_time_steps = int(total_evolution_time/delta_t)\n", "\n", "times = [0]\n", - "vals = [[initial_state.dot(sigmaz(i)*initial_state).real for i in range(config.L)]]\n", + "vals = [[Szi.expectation(initial_state) for Szi in Sz_ops]]\n", "\n", "result = initial_state.copy() # pre-allocating the result vector improves performance\n", "for i in range(n_time_steps):\n", " times.append(times[-1] + delta_t)\n", " H.evolve(initial_state, t=delta_t, result=result)\n", - " vals.append([result.dot(sigmaz(i)*result).real for i in range(config.L)])\n", + " vals.append([Szi.expectation(result) for Szi in Sz_ops])\n", " \n", " # swap initial state and result\n", " # because the previous result becomes the next initial state\n", @@ -160,7 +176,9 @@ "cell_type": "code", "execution_count": null, "id": "1a245bf2-9f47-4d41-b090-a658b864fcf0", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# plot the trajectory of the magnetization for each spin\n", @@ -218,7 +236,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index aae2118..f46f5ec 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -765,6 +765,29 @@ def create_states(self): ket = State(subspace=self.right_subspace) return (bra, ket) + def expectation(self, state, tmp_state=None): + ''' + Convenience function to compute the expectation value with respect + to a given state. + + Parameters + ---------- + state : dynamite.states.State + The state for which to compute the expectation value + + tmp_state : dynamite.states.State, optional + A "scratch space" state to use during the computation. Must be in the correct + subspace. A state vector is allocated internally if one is not provided. + ''' + if tmp_state is None: + tmp_state = self.dot(state) + else: + self.dot(state, result=tmp_state) + + # operators in dynamite are always Hermitian, so we can just + # return the real part only + return state.dot(tmp_state).real + ### mask, sign, coefficient representation of operators @property diff --git a/tests/integration/test_operators.py b/tests/integration/test_operators.py index 853ef9b..1a587ee 100644 --- a/tests/integration/test_operators.py +++ b/tests/integration/test_operators.py @@ -12,6 +12,7 @@ from dynamite.subspaces import Full, Parity, SpinConserve, Auto, XParity from dynamite.operators import index_sum, sigmax, sigmay, sigmaz from dynamite.operators import Operator +from dynamite.states import State, UninitializedError import hamiltonians @@ -259,6 +260,37 @@ def test_random_coeff_fail(self): op.build_mat() +class Expectation(dtr.DynamiteTestCase): + @classmethod + def expectation_correct(cls, H, state): + tmp = H*state + return state.dot(tmp).real + + def test_simple(self): + for H_name in hamiltonians.get_names(complex_enabled()): + with self.subTest(H=H_name): + H = getattr(hamiltonians, H_name)() + state = State(state='random') + correct = self.expectation_correct(H, state) + + self.assertEqual( + H.expectation(state), + correct + ) + + tmp = State() + self.assertEqual( + H.expectation(state, tmp_state=tmp), + correct + ) + + def test_uninitialized_fail(self): + with self.assertRaises(UninitializedError): + H = hamiltonians.ising() + state = State() + H.expectation(state) + + class Exceptions(dtr.DynamiteTestCase): def test_scale(self): From e07725bab63d7631559e62434321e092a16c7cc7 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 20 Mar 2023 11:40:08 -0700 Subject: [PATCH 06/73] rename msc_size -> nterms --- CHANGELOG.md | 3 +++ benchmarking/benchmark.py | 2 +- src/dynamite/operators.py | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 622f7bc..dbe8094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ### Added - `Operator.expectation()`, convenience function to compute the expectation value of the operator with respect to a state +### Changed + - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` + ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index a6915f7..204fba5 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -246,7 +246,7 @@ def main(): Print(' dim:', H.dim[0]) Print(' nnz:', H.nnz) Print(' density:', H.density) - Print(' MSC size:', H.msc_size) + Print(' nterms:', H.nterms) log_call(H.build_mat, stats)() # build some states to use in the computations diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index f46f5ec..dbb8507 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -205,12 +205,22 @@ def nnz(self): return msc_tools.nnz(self.msc) @property - def msc_size(self): + def nterms(self): """ - The number of elements in the MSC representation of the matrix. + The number of terms in the operator, when written as a sum of products of Paulis. + Note that Operator.reduce_msc() is called when accessing this property, which cleans + up and sorts the internal representation used by dynamite for the operator. """ + self.reduce_msc() return len(self.msc) + @property + def msc_size(self): + """ + (deprecated) + """ + raise DeprecationWarning('Operator.msc_size is deprecated, use Operator.nterms instead') + @property def density(self): """ From 30e2cc03460b75a7fc3e839668f7d796153a126f Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 20 Mar 2023 11:49:15 -0700 Subject: [PATCH 07/73] do nothing when Operator.scale() is called with a scale factor of 1 --- src/dynamite/operators.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index dbb8507..5eb6f0b 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -1119,6 +1119,9 @@ def scale(self, x): x : numeric type The coefficient to scale by ''' + if x == 1: + return + try: self.msc['coeffs'] *= x except (ValueError, TypeError): From 11ba359271d7244852376a2616a0ae4d66cf6295 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 21 Mar 2023 11:19:19 -0700 Subject: [PATCH 08/73] add tools.MPI_COMM_WORLD() --- CHANGELOG.md | 1 + src/dynamite/operators.py | 22 ++++++++++----------- src/dynamite/states.py | 41 ++++++++++++++++----------------------- src/dynamite/tools.py | 24 ++++++++++++++++++----- 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe8094..8ffcb37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - `Operator.expectation()`, convenience function to compute the expectation value of the operator with respect to a state + - `dynamite.tools.MPI_COMM_WORLD()` which returns PETSc's MPI communicator object ### Changed - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index 5eb6f0b..287a86b 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -13,7 +13,8 @@ from .computations import evolve, eigsolve from .subspaces import Full, Explicit, XParity from .states import State -from .tools import complex_enabled +from .tools import complex_enabled, MPI_COMM_WORLD + class Operator: """ @@ -517,13 +518,12 @@ def save(self, filename): filename : str The path to the file to save the operator in. """ - config._initialize() - from petsc4py import PETSc - if PETSc.COMM_WORLD.rank == 0: + comm = MPI_COMM_WORLD() + if comm.rank == 0: with open(filename, mode='wb') as f: f.write(self.serialize()) - PETSc.COMM_WORLD.barrier() + comm.barrier() @classmethod def load(cls, filename): @@ -633,14 +633,14 @@ def build_mat(self, subspaces=None): @classmethod def _check_consistent_msc(cls, msc): - config._initialize() - from petsc4py import PETSc + comm = MPI_COMM_WORLD() # msc cannot be inconsistent with only 1 rank - if PETSc.COMM_WORLD.size == 1: + if comm.size == 1: return - comm = PETSc.COMM_WORLD.tompi4py() + # otherwise we need the full machinery of mpi4py + comm = comm.tompi4py() checksum = crc32(msc.data) all_checksums = comm.allgather(checksum) @@ -716,9 +716,7 @@ def estimate_memory(self, mpi_size=None): The expected memory usage, in gigabytes ''' if mpi_size is None: - config._initialize() - from petsc4py import PETSc - mpi_size = PETSc.COMM_WORLD.size + mpi_size = MPI_COMM_WORLD().size if self.shell: usage_bytes = self.msc.nbytes diff --git a/src/dynamite/states.py b/src/dynamite/states.py index ae44210..0faaa61 100644 --- a/src/dynamite/states.py +++ b/src/dynamite/states.py @@ -1,6 +1,6 @@ from . import config, validate, subspaces -from .tools import complex_enabled +from .tools import complex_enabled, MPI_COMM_WORLD from .msc_tools import dnm_int_t import numpy as np @@ -252,24 +252,22 @@ def set_uniform(self): @classmethod def generate_time_seed(cls): + comm = MPI_COMM_WORLD() - config._initialize() - from petsc4py import PETSc - - if PETSc.COMM_WORLD.size == 1: + if comm.size == 1: # in this case it works without needing mpi4py return int(time()) else: # otherwise we have to coordinate among the ranks - CW = PETSc.COMM_WORLD.tompi4py() + comm = comm.tompi4py() - if CW.rank == 0: + if comm.rank == 0: seed = int(time()) else: seed = None - return CW.bcast(seed, root = 0) + return comm.bcast(seed, root=0) def set_random(self, seed = None, normalize = True): """ @@ -289,10 +287,6 @@ def set_random(self, seed = None, normalize = True): normalize : bool Whether to rescale the random state to have norm 1. """ - - config._initialize() - from petsc4py import PETSc - istart, iend = self.vec.getOwnershipRange() R = np.random.RandomState() @@ -305,7 +299,7 @@ def set_random(self, seed = None, normalize = True): # if my code is still being used in year 2038, wouldn't want it to # overflow numpy's PRNG seed range ;) - R.seed((seed + PETSc.COMM_WORLD.rank) % 2**32) + R.seed((seed + MPI_COMM_WORLD().rank) % 2**32) local_size = iend-istart @@ -426,11 +420,11 @@ def _to_numpy(cls, vec, to_all = False): A numpy array of the vector, or ``None`` on all processes other than 0 if `to_all == False`. ''' - + config._initialize() from petsc4py import PETSc # scatter seems to be broken for CUDA vectors - if PETSc.COMM_WORLD.size > 1: + if MPI_COMM_WORLD().size > 1: # collect to process 0 if to_all: sc, v0 = PETSc.Scatter.toAll(vec) @@ -440,7 +434,7 @@ def _to_numpy(cls, vec, to_all = False): sc.begin(vec, v0) sc.end(vec, v0) - if not to_all and PETSc.COMM_WORLD.rank != 0: + if not to_all and MPI_COMM_WORLD().rank != 0: return None else: v0 = vec @@ -476,13 +470,12 @@ def _get_nonzero_elements(self): ''' self.assert_initialized() + comm = MPI_COMM_WORLD() + # get the functions we need, but without requiring mpi4py # if we're running on only 1 rank - config._initialize() - from petsc4py import PETSc - if PETSc.COMM_WORLD.size > 1: - allgather = PETSc.COMM_WORLD.tompi4py().allgather - + if comm.size > 1: + allgather = comm.tompi4py().allgather start, end = self.vec.getOwnershipRange() local_vec = np.ndarray((end-start,), dtype=np.complex128) local_vec[:] = self.vec[start:end] @@ -499,11 +492,11 @@ def _get_nonzero_elements(self): # only take the first 3 and final 1 n_nonzero_preceding = 0 - for rank in range(0, PETSc.COMM_WORLD.rank): + for rank in range(0, comm.rank): n_nonzero_preceding += all_nonzero[rank] n_nonzero_after = 0 - for rank in range(PETSc.COMM_WORLD.size-1, PETSc.COMM_WORLD.rank, -1): + for rank in range(comm.size-1, comm.rank, -1): n_nonzero_after += all_nonzero[rank] local_take_indices = [] @@ -645,7 +638,7 @@ def save(self, fname): config._initialize() from petsc4py import PETSc - if PETSc.COMM_WORLD.rank == 0: + if MPI_COMM_WORLD().rank == 0: with open(fname+'.metadata', 'wb') as f: pickle.dump(self.subspace, f) diff --git a/src/dynamite/tools.py b/src/dynamite/tools.py index ce3fea7..03c6e1d 100644 --- a/src/dynamite/tools.py +++ b/src/dynamite/tools.py @@ -2,6 +2,18 @@ Various tools useful for writing and analyzing dynamite programs. ''' + +def MPI_COMM_WORLD(): + ''' + Returns PETSc's COMM_WORLD object. Can be converted to an mpi4py + communicator object via the ``.tompi4py()`` method. + ''' + from . import config + config._initialize() # dynamite must be initialized before importing PETSc + from petsc4py import PETSc + return PETSc.COMM_WORLD + + def mpi_print(*args, rank=0, **kwargs): ''' Print from only a single MPI rank, default rank 0. @@ -9,13 +21,10 @@ def mpi_print(*args, rank=0, **kwargs): Aside from the extra "rank" keywork argument, call signature is the same as Python 3's ``print()`` function. ''' - from . import config - config._initialize() - from petsc4py import PETSc - - if PETSc.COMM_WORLD.rank == rank: + if MPI_COMM_WORLD().rank == rank: print(*args, **kwargs) + def get_version(): ''' Gets the version information for dynamite, and the PETSc and SLEPc libraries it's built on. @@ -43,6 +52,7 @@ def get_version(): rtn['dynamite']['version'] = bbuild.get_build_version() return rtn + def get_version_str(): ''' Get a string with the version information for PETSc, SLEPc, and dynamite. @@ -70,6 +80,7 @@ def get_version_str(): f'"{dnm_branch}") built with PETSc {PETSc_v} and SLEPc {SLEPc_v}' return rtn + def track_memory(): ''' Begin tracking memory usage for a later call to :meth:`get_max_memory_usage`. @@ -79,6 +90,7 @@ def track_memory(): from ._backend import bpetsc return bpetsc.track_memory() + def get_max_memory_usage(which='all'): ''' Get the maximum memory usage up to this point, in gigabytes. @@ -106,6 +118,7 @@ def get_max_memory_usage(which='all'): from ._backend import bpetsc return bpetsc.get_max_memory_usage(which=which)/1E9 + def get_cur_memory_usage(which='all'): ''' Get the current memory usage (resident set size) in gigabytes. @@ -126,6 +139,7 @@ def get_cur_memory_usage(which='all'): from ._backend import bpetsc return bpetsc.get_cur_memory_usage(which=which)/1E9 + def complex_enabled(): from ._backend import bbuild return bbuild.complex_enabled() From c84b3763cadb16e0d67be0a7c304d32a301ba433 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 21 Mar 2023 14:39:46 -0700 Subject: [PATCH 09/73] include \limits in sum and product LaTeX --- src/dynamite/operators.py | 4 ++-- tests/unit/test_operators.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index 287a86b..20c3762 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -1413,7 +1413,7 @@ def index_sum(op, size = None, start = 0, boundary = 'open'): sub_tex = sub_tex.replace('{IDX', '{IDX'+idx+'+') sub_tex = sub_tex.replace('{IDX'+idx+'+0', '{IDX'+idx) - rtn._string_rep.tex = r'\sum_{'+idx+'=%d}^{%d}' % (start, stop-1) + sub_tex + rtn._string_rep.tex = r'\sum\limits_{'+idx+'=%d}^{%d}' % (start, stop-1) + sub_tex rtn._string_rep.brackets = '[]' return rtn @@ -1461,7 +1461,7 @@ def index_product(op, size = None, start = 0): idx = _get_next_index(sub_tex) sub_tex = sub_tex.replace('{IDX', '{IDX'+idx+'+') sub_tex = sub_tex.replace('{IDX'+idx+'+0', '{IDX'+idx) - rtn._string_rep.tex = r'\prod_{'+idx+'=%d}^{%d}' % (start, stop-1) + rtn._string_rep.tex = r'\prod\limits_{'+idx+'=%d}^{%d}' % (start, stop-1) rtn._string_rep.tex += sub_tex rtn._string_rep.brackets = '[]' diff --git a/tests/unit/test_operators.py b/tests/unit/test_operators.py index 1d10227..1e85276 100644 --- a/tests/unit/test_operators.py +++ b/tests/unit/test_operators.py @@ -553,7 +553,7 @@ def test_tex(self): op = index_sum(index_product(sigmax(), size=3), size=8) self.assertEqual( op._repr_latex_(), - r'$\sum_{j=0}^{5}\left[\prod_{i=0}^{2}\sigma^x_{j+i}\right]$' + r'$\sum\limits_{j=0}^{5}\left[\prod\limits_{i=0}^{2}\sigma^x_{j+i}\right]$' ) From 05098f29092b4b1e7d1d122bedd8db23eec92a84 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 21 Mar 2023 15:02:53 -0700 Subject: [PATCH 10/73] add first draft of example scripts --- docs/index.rst | 2 + examples/scripts/MBL/README.ipynb | 229 ++++++++++ examples/scripts/MBL/README.md | 117 ++++++ examples/scripts/MBL/plot_results.py | 45 ++ examples/scripts/MBL/run_mbl.py | 163 +++++++ examples/scripts/README.md | 99 +++++ examples/scripts/SYK/README.ipynb | 396 ++++++++++++++++++ examples/scripts/SYK/README.md | 198 +++++++++ examples/scripts/SYK/run_syk.py | 271 ++++++++++++ examples/scripts/floquet/README.ipynb | 234 +++++++++++ examples/scripts/floquet/README.md | 126 ++++++ examples/scripts/floquet/run_floquet.py | 187 +++++++++ examples/scripts/kagome/README.ipynb | 197 +++++++++ examples/scripts/kagome/README.md | 87 ++++ .../kagome/README_files/README_2_0.png | Bin 0 -> 53207 bytes examples/scripts/kagome/lattice_library.py | 170 ++++++++ examples/scripts/kagome/plot_lattice.py | 81 ++++ examples/scripts/kagome/run_kagome.py | 104 +++++ examples/tutorial/0-Welcome.ipynb | 6 +- examples/tutorial/7-Conclusion.ipynb | 14 +- 20 files changed, 2713 insertions(+), 13 deletions(-) create mode 100644 examples/scripts/MBL/README.ipynb create mode 100644 examples/scripts/MBL/README.md create mode 100644 examples/scripts/MBL/plot_results.py create mode 100644 examples/scripts/MBL/run_mbl.py create mode 100644 examples/scripts/README.md create mode 100644 examples/scripts/SYK/README.ipynb create mode 100644 examples/scripts/SYK/README.md create mode 100644 examples/scripts/SYK/run_syk.py create mode 100644 examples/scripts/floquet/README.ipynb create mode 100644 examples/scripts/floquet/README.md create mode 100644 examples/scripts/floquet/run_floquet.py create mode 100644 examples/scripts/kagome/README.ipynb create mode 100644 examples/scripts/kagome/README.md create mode 100644 examples/scripts/kagome/README_files/README_2_0.png create mode 100644 examples/scripts/kagome/lattice_library.py create mode 100644 examples/scripts/kagome/plot_lattice.py create mode 100644 examples/scripts/kagome/run_kagome.py diff --git a/docs/index.rst b/docs/index.rst index 9f29128..75aa162 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,8 @@ To run the tutorial, `install Docker `_ Then follow the last link in the output (it should start with ``http://127.0.0.1:8887``). Start the tutorial by launching the notebook ``0-Welcome.ipynb`` in the left panel. +You may also be interested in looking at dynamite's `example scripts `_. + .. note:: dynamite is in beta! You may find bugs. When you do, please submit them on the `GitHub Issues `_ diff --git a/examples/scripts/MBL/README.ipynb b/examples/scripts/MBL/README.ipynb new file mode 100644 index 0000000..6193600 --- /dev/null +++ b/examples/scripts/MBL/README.ipynb @@ -0,0 +1,229 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "85163e6d-0dad-4ade-9b57-fbbd27cf41f9", + "metadata": {}, + "source": [ + "# Many-body localization\n", + "\n", + "## In this example\n", + "\n", + " - Eigensolving for ground states\n", + " - Eigensolving for states in the middle of the spectrum\n", + " - The `SpinConserve` subspace\n", + " - Computing entanglement entropy\n", + " - Coordinating randomness across MPI ranks" + ] + }, + { + "cell_type": "markdown", + "id": "3902ad9f-31fc-42eb-9d1a-8c4db28b914b", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "This project uses dynamite to explore many-body localization (MBL), a surprising phenomenon in which certain many-body systems with sufficiently strong disorder fail to thermalize. In particular we will study the *MBL transition*, in which the system moves from thermalizing to localized as the disorder strength is increased. Characterization of this transition has remained elusive: finite size effects seem hard to avoid, and tensor network methods break down due to extensive entanglement in states near the transition. Thus, iterative methods like the Krylov subspace methods used by dynamite have proved to be one of the best tools for its study.[1](#ref1) We refer readers interested in learning more about the physics of MBL to one of the excellent review papers on the subject.[2](#ref2)\n", + "\n", + "In this project we explore a model of nearest-neighbor Heisenberg interactions on a 1D chain, with disorder implemented as random Z fields on each site:\n", + "\n", + "$$H = \\sum_{\\left< i,j \\right> } \\vec{S}_i \\cdot \\vec{S}_j + \\sum_i h_i S^z_i$$\n", + "\n", + "where $\\vec{S} = (S^x, S^y, S^z)$, the subscripts indicate the index of the spin in the chain, and the angle brackets indicate that the indices run over nearest neighbors. The values of $h_i$ are drawn from a uniform distribution $\\left[-h, h\\right]$ where $h$ is a parameter that controls the strength of the disorder.\n", + "\n", + "In the script `run_mbl.py` this is implemented by the `build_hamiltonian` function:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b02d267b-a5e9-43a8-b51b-982bb7210429", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\sum\\limits_{i=0}^{4}0.25\\left(\\sigma^x_{i}\\sigma^x_{i+1} + \\sigma^y_{i}\\sigma^y_{i+1} + \\sigma^z_{i}\\sigma^z_{i+1}\\right) + 0.855\\sigma^z_{0} + 0.143\\sigma^z_{1} + 0.307\\sigma^z_{2} + 0.398\\sigma^z_{3} + -0.776\\sigma^z_{4} + 0.875\\sigma^z_{5}$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dynamite import config\n", + "config.L = 6\n", + "\n", + "from run_mbl import build_hamiltonian\n", + "build_hamiltonian(h=2)" + ] + }, + { + "cell_type": "markdown", + "id": "ffb58459-eb36-43da-a1b4-f1fa298d80ed", + "metadata": {}, + "source": [ + "## Goals" + ] + }, + { + "cell_type": "markdown", + "id": "0df56ad3-206f-4799-98f9-26cc9b69ecda", + "metadata": {}, + "source": [ + "In this project we plot two quantities that help us identify the MBL transition: the half-chain entanglement entropy of eigenstates, and an eigenvalue statistic called the *adjacent gap ratio*. The half chain entanglement entropy $S_{L/2}$ is simply the bipartite von Neumann entropy when half the spins are traced out. The MBL transition should correspond to a transition from volume law to area law entanglement. The adjacent gap ratio computes a measure of how likely eigenvalues are to be near each other. Let $\\Delta_i$ be the gap between the $i^\\mathrm{th}$ eigenvalue and the following one; that is, $\\Delta_i = \\lambda_{i+1} - \\lambda_i$. Then, the adjacent gap ratio is defined as $r_i = \\frac{\\min \\left( \\Delta_i, \\Delta_{i+1} \\right)}{\\max \\left( \\Delta_i, \\Delta_{i+1} \\right)}$. Random matrix theory tells us that in the thermal phase, the expectation value of $r$ should be $\\left< r \\right> \\approx 0.53$, while in the localized phase $\\left< r \\right> \\approx 0.39$.\n", + "\n", + "The key feature that makes MBL so interesting is that the transition from volume to area law entanglement does not only occur in the ground state, but in excited states as well. This presents a great use case for dynamite's `target` eigenvalue solver, which finds the $k$ eigenvalues (and eigenvectors) closest to a target energy, where $k$ is user configurable. So, the plan is as follows: we will choose a few energies at various points in the spectrum, solve for some reasonable number (say, 32) eigenpairs near each of those points, and then compute the entanglement entropy and adjacent gap ratio for all of those eigenpairs." + ] + }, + { + "cell_type": "markdown", + "id": "2fb6d175-b7b2-4a33-b36e-b47c45cab9cb", + "metadata": {}, + "source": [ + "## Remark: details of the solver for excited states\n", + "\n", + "The iterative solver used by dynamite to solve for eigenpairs is very good at finding *extremal* eigenvalues---those with the largest absolute value. To apply this solver to finding sets of interior eigenvalues, dynamite uses what's called the *shift-invert* transformation. Instead of applying the solver to the Hamiltonian $H$ itself, it is applied to the transformed Hamiltonian $(H-E_\\mathrm{targ})^{-1}$, where $E_\\mathrm{targ}$ is the target energy near which eigenpairs are desired. With this transformation, the eigenvalues closest to $E_\\mathrm{targ}$ become extremal.\n", + "\n", + "To apply the iterative solver, we need to be able to efficiently compute matrix vector products of the form $(H-E_\\mathrm{targ})^{-1} \\vec{x}$. To implement this, PETSc (the linear algebra library that underlies dynamite) computes the LU factorization of $H$---a pair of matrices $L$ and $U$ where $L$ is lower triangular and $U$ is upper triangular, and $H = LU$. This factorization makes it very easy to perform the linear solves needed to apply the matrix inverse, but it comes with multiple costs. For one, the $LU$ factorization generally requires significantly more memory to store than $H$ itself. Furthermore, computing the $LU$ factorization can be computationally expensive.\n", + "\n", + "Thus, you will find solving for interior eigenvalues to be much more computationally intensive than solving for ground states." + ] + }, + { + "cell_type": "markdown", + "id": "a8dd5c0c-7a7a-4a81-99ae-9d362a7d0047", + "metadata": {}, + "source": [ + "## Remark: disorder realizations and parallelism\n", + "\n", + "(also discussed in the SYK example)\n", + "\n", + "The MBL Hamiltonian is a case in which getting good data requires disorder averaging---that is, running the computation many times with fresh randomness. Given $N$ CPU cores there are two main ways one can parallelize that process: (1) running $N$ disorder realizations independently at the same time, each using one core, and (2) using MPI to parallelize one computation across all $N$ cores and then doing each disorder realization in sequence. In this case, (1) will almost always be faster and should be prioritized---while the MPI parallelism in dynamite is highly optimized, there will always be some cost to the communication between cores.\n", + "\n", + "However, there are situations in which using MPI may be preferable, for example if running $N$ independent disorder realizations uses too much memory. Ultimately, the user should experiment with different configurations to determine what gives the best performance. Unfortunately, PETSc currently only supports performing part of the shift-invert computation on GPUs, so for this computation GPU performance is bottlenecked (hopefully this will change soon)." + ] + }, + { + "cell_type": "markdown", + "id": "83a3889f-b710-45f9-8c08-7bed51fc9769", + "metadata": {}, + "source": [ + "## Remark: using randomness in dynamite\n", + "\n", + "(also discussed in SYK example)\n", + "\n", + "One needs to take extra care when using randomness in code that will be run under MPI with multiple ranks. Each rank is its own Python process, and by default will have a different random seed---so if you are not careful, each of your ranks may end up trying to build a different Hamiltonian! (dynamite does check that the Hamiltonian is the same on all ranks before building the underlying matrix, so you will get an error if this happens).\n", + "\n", + "There are two ways to handle this: one is to have rank 0 pick a random seed and use MPI to communicate it to all the other ranks, and the other is to simply pass a seed on the command line. Both are implemented in this example for demonstration purposes: if no seed is passed on the command line, then one is generated and communicated to all MPI ranks. If you pass a random seed on the command line, make sure to change it each time you run the code if you want new disorder realizations!\n", + "\n", + "**Note 1:** when setting a random state via `State(state='random')`, dynamite is already careful about coordinating randomness between MPI ranks, so the user does not need to worry about it in that case.\n", + "\n", + "**Note 2:** If you will never run your code on multiple MPI ranks, you don't need to worry about this at all. In particular, running on a GPU with 1 CPU process will not encounter this issue." + ] + }, + { + "cell_type": "markdown", + "id": "d973d804-568c-4b3f-8933-961cb58aa63f", + "metadata": {}, + "source": [ + "## Usage" + ] + }, + { + "cell_type": "markdown", + "id": "388a6873-ec57-4d5a-9227-ccf0a3b195d4", + "metadata": {}, + "source": [ + "The computation is implemented in `run_mbl.py`. The script will output, in CSV format, statistics of eigenpairs clustered around equally-spaced points throughout the spectrum. \n", + "\n", + "This example also includes a script `plot_result.py` which can be used to plot the results using matplotlib. Data can either be piped directly to it:\n", + "```bash\n", + "python run_mbl.py -L 14 | python plot_result.py\n", + "```\n", + "or one can save the data to a file and pass the filename on the command line:\n", + "```bash\n", + "python run_mbl.py -L 14 > output.csv\n", + "python plot_result.py output.csv\n", + "```\n", + "\n", + "We also provide an example output file `example_output.csv` which was run with... # TODO: update details when job finishes\n", + "\n", + "Here are the command line options for `run_mbl.py`:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "85ed7837-2892-4858-baa2-7fe413e83991", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: run_mbl.py [-h] -L L [--seed SEED] [--iters ITERS]\n", + " [--energy-points ENERGY_POINTS] [--h-points H_POINTS]\n", + " [--h-min H_MIN] [--h-max H_MAX] [--nev NEV]\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " -L L spin chain length\n", + " --seed SEED seed for random number generator. if omitted, a random\n", + " seed is chosen by querying system hardware randomness\n", + " --iters ITERS number of disorder realizations\n", + " --energy-points ENERGY_POINTS\n", + " number of points in the spectrum to target\n", + " --h-points H_POINTS number of disorder strengths to test\n", + " --h-min H_MIN minimum value of disorder strength h\n", + " --h-max H_MAX maximum value of disorder strength h\n", + " --nev NEV number of eigenpairs to compute at each point\n" + ] + } + ], + "source": [ + "! python run_mbl.py -h" + ] + }, + { + "cell_type": "markdown", + "id": "8ce2029c-d7f7-4f15-80e3-1a9bacf0bff9", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1 [Pietracaprina et al., \"Shift-invert diagonalization of large many-body localizing spin chains\"](https://doi.org/10.21468/SciPostPhys.5.5.045) \n", + "2 [Abanin et al., \"Many-body localization, thermalization, and entanglement\"](https://doi.org/10.1103/RevModPhys.91.021001)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/scripts/MBL/README.md b/examples/scripts/MBL/README.md new file mode 100644 index 0000000..09a2761 --- /dev/null +++ b/examples/scripts/MBL/README.md @@ -0,0 +1,117 @@ +# Many-body localization + +## In this example + + - Eigensolving for ground states + - Eigensolving for states in the middle of the spectrum + - The `SpinConserve` subspace + - Computing entanglement entropy + - Coordinating randomness across MPI ranks + +## Overview + +This project uses dynamite to explore many-body localization (MBL), a surprising phenomenon in which certain many-body systems with sufficiently strong disorder fail to thermalize. In particular we will study the *MBL transition*, in which the system moves from thermalizing to localized as the disorder strength is increased. Characterization of this transition has remained elusive: finite size effects seem hard to avoid, and tensor network methods break down due to extensive entanglement in states near the transition. Thus, iterative methods like the Krylov subspace methods used by dynamite have proved to be one of the best tools for its study.[1](#ref1) We refer readers interested in learning more about the physics of MBL to one of the excellent review papers on the subject.[2](#ref2) + +In this project we explore a model of nearest-neighbor Heisenberg interactions on a 1D chain, with disorder implemented as random Z fields on each site: + +$$H = \sum_{\left< i,j \right> } \vec{S}_i \cdot \vec{S}_j + \sum_i h_i S^z_i$$ + +where $\vec{S} = (S^x, S^y, S^z)$, the subscripts indicate the index of the spin in the chain, and the angle brackets indicate that the indices run over nearest neighbors. The values of $h_i$ are drawn from a uniform distribution $\left[-h, h\right]$ where $h$ is a parameter that controls the strength of the disorder. + +In the script `run_mbl.py` this is implemented by the `build_hamiltonian` function: + + +```python +from dynamite import config +config.L = 6 + +from run_mbl import build_hamiltonian +build_hamiltonian(h=2) +``` + + + + +$\sum\limits_{i=0}^{4}0.25\left(\sigma^x_{i}\sigma^x_{i+1} + \sigma^y_{i}\sigma^y_{i+1} + \sigma^z_{i}\sigma^z_{i+1}\right) + 0.855\sigma^z_{0} + 0.143\sigma^z_{1} + 0.307\sigma^z_{2} + 0.398\sigma^z_{3} + -0.776\sigma^z_{4} + 0.875\sigma^z_{5}$ + + + +## Goals + +In this project we plot two quantities that help us identify the MBL transition: the half-chain entanglement entropy of eigenstates, and an eigenvalue statistic called the *adjacent gap ratio*. The half chain entanglement entropy $S_{L/2}$ is simply the bipartite von Neumann entropy when half the spins are traced out. The MBL transition should correspond to a transition from volume law to area law entanglement. The adjacent gap ratio computes a measure of how likely eigenvalues are to be near each other. Let $\Delta_i$ be the gap between the $i^\mathrm{th}$ eigenvalue and the following one; that is, $\Delta_i = \lambda_{i+1} - \lambda_i$. Then, the adjacent gap ratio is defined as $r_i = \frac{\min \left( \Delta_i, \Delta_{i+1} \right)}{\max \left( \Delta_i, \Delta_{i+1} \right)}$. Random matrix theory tells us that in the thermal phase, the expectation value of $r$ should be $\left< r \right> \approx 0.53$, while in the localized phase $\left< r \right> \approx 0.39$. + +The key feature that makes MBL so interesting is that the transition from volume to area law entanglement does not only occur in the ground state, but in excited states as well. This presents a great use case for dynamite's `target` eigenvalue solver, which finds the $k$ eigenvalues (and eigenvectors) closest to a target energy, where $k$ is user configurable. So, the plan is as follows: we will choose a few energies at various points in the spectrum, solve for some reasonable number (say, 32) eigenpairs near each of those points, and then compute the entanglement entropy and adjacent gap ratio for all of those eigenpairs. + +## Remark: details of the solver for excited states + +The iterative solver used by dynamite to solve for eigenpairs is very good at finding *extremal* eigenvalues---those with the largest absolute value. To apply this solver to finding sets of interior eigenvalues, dynamite uses what's called the *shift-invert* transformation. Instead of applying the solver to the Hamiltonian $H$ itself, it is applied to the transformed Hamiltonian $(H-E_\mathrm{targ})^{-1}$, where $E_\mathrm{targ}$ is the target energy near which eigenpairs are desired. With this transformation, the eigenvalues closest to $E_\mathrm{targ}$ become extremal. + +To apply the iterative solver, we need to be able to efficiently compute matrix vector products of the form $(H-E_\mathrm{targ})^{-1} \vec{x}$. To implement this, PETSc (the linear algebra library that underlies dynamite) computes the LU factorization of $H$---a pair of matrices $L$ and $U$ where $L$ is lower triangular and $U$ is upper triangular, and $H = LU$. This factorization makes it very easy to perform the linear solves needed to apply the matrix inverse, but it comes with multiple costs. For one, the $LU$ factorization generally requires significantly more memory to store than $H$ itself. Furthermore, computing the $LU$ factorization can be computationally expensive. + +Thus, you will find solving for interior eigenvalues to be much more computationally intensive than solving for ground states. + +## Remark: disorder realizations and parallelism + +(also discussed in the SYK example) + +The MBL Hamiltonian is a case in which getting good data requires disorder averaging---that is, running the computation many times with fresh randomness. Given $N$ CPU cores there are two main ways one can parallelize that process: (1) running $N$ disorder realizations independently at the same time, each using one core, and (2) using MPI to parallelize one computation across all $N$ cores and then doing each disorder realization in sequence. In this case, (1) will almost always be faster and should be prioritized---while the MPI parallelism in dynamite is highly optimized, there will always be some cost to the communication between cores. + +However, there are situations in which using MPI may be preferable, for example if running $N$ independent disorder realizations uses too much memory. Ultimately, the user should experiment with different configurations to determine what gives the best performance. Unfortunately, PETSc currently only supports performing part of the shift-invert computation on GPUs, so for this computation GPU performance is bottlenecked (hopefully this will change soon). + +## Remark: using randomness in dynamite + +(also discussed in SYK example) + +One needs to take extra care when using randomness in code that will be run under MPI with multiple ranks. Each rank is its own Python process, and by default will have a different random seed---so if you are not careful, each of your ranks may end up trying to build a different Hamiltonian! (dynamite does check that the Hamiltonian is the same on all ranks before building the underlying matrix, so you will get an error if this happens). + +There are two ways to handle this: one is to have rank 0 pick a random seed and use MPI to communicate it to all the other ranks, and the other is to simply pass a seed on the command line. Both are implemented in this example for demonstration purposes: if no seed is passed on the command line, then one is generated and communicated to all MPI ranks. If you pass a random seed on the command line, make sure to change it each time you run the code if you want new disorder realizations! + +**Note 1:** when setting a random state via `State(state='random')`, dynamite is already careful about coordinating randomness between MPI ranks, so the user does not need to worry about it in that case. + +**Note 2:** If you will never run your code on multiple MPI ranks, you don't need to worry about this at all. In particular, running on a GPU with 1 CPU process will not encounter this issue. + +## Usage + +The computation is implemented in `run_mbl.py`. The script will output, in CSV format, statistics of eigenpairs clustered around equally-spaced points throughout the spectrum. + +This example also includes a script `plot_result.py` which can be used to plot the results using matplotlib. Data can either be piped directly to it: +```bash +python run_mbl.py -L 14 | python plot_result.py +``` +or one can save the data to a file and pass the filename on the command line: +```bash +python run_mbl.py -L 14 > output.csv +python plot_result.py output.csv +``` + +We also provide an example output file `example_output.csv` which was run with... # TODO: update details when job finishes + +Here are the command line options for `run_mbl.py`: + + +```python +! python run_mbl.py -h +``` + + usage: run_mbl.py [-h] -L L [--seed SEED] [--iters ITERS] + [--energy-points ENERGY_POINTS] [--h-points H_POINTS] + [--h-min H_MIN] [--h-max H_MAX] [--nev NEV] + + options: + -h, --help show this help message and exit + -L L spin chain length + --seed SEED seed for random number generator. if omitted, a random + seed is chosen by querying system hardware randomness + --iters ITERS number of disorder realizations + --energy-points ENERGY_POINTS + number of points in the spectrum to target + --h-points H_POINTS number of disorder strengths to test + --h-min H_MIN minimum value of disorder strength h + --h-max H_MAX maximum value of disorder strength h + --nev NEV number of eigenpairs to compute at each point + + +## References + +1 [Pietracaprina et al., "Shift-invert diagonalization of large many-body localizing spin chains"](https://doi.org/10.21468/SciPostPhys.5.5.045) +2 [Abanin et al., "Many-body localization, thermalization, and entanglement"](https://doi.org/10.1103/RevModPhys.91.021001) diff --git a/examples/scripts/MBL/plot_results.py b/examples/scripts/MBL/plot_results.py new file mode 100644 index 0000000..b06ba73 --- /dev/null +++ b/examples/scripts/MBL/plot_results.py @@ -0,0 +1,45 @@ + +import fileinput +from csv import DictReader +from collections import defaultdict +from matplotlib import pyplot as plt + + +def read_data(): + reader = DictReader(fileinput.input()) + rtn = defaultdict(lambda: defaultdict(list)) + for row in reader: + # average over data points + key = (float(row['h']), float(row['energy_point'])) + for k in ('entropy', 'ratio'): + rtn[key][k].append(float(row[k])) + + # average + for data_pt in rtn.values(): + for k, v in data_pt.items(): + data_pt[k] = sum(v)/len(v) + + return rtn + + +def plot_data(data): + f, axes = plt.subplots(2, 1, sharex=True) + + for metric, ax in zip(['entropy', 'ratio'], axes): + for energy_pt in sorted(set(e for _, e in data)): + x, y = zip(*((h, v[metric]) for (h, e), v in data.items() if e == energy_pt)) + ax.plot(x, y) + ax.set_ylabel(metric) + + plt.xlabel('Disorder strength $h$') + + plt.show() + + +def main(): + data = read_data() + plot_data(data) + + +if __name__ == '__main__': + main() diff --git a/examples/scripts/MBL/run_mbl.py b/examples/scripts/MBL/run_mbl.py new file mode 100644 index 0000000..dddf3bc --- /dev/null +++ b/examples/scripts/MBL/run_mbl.py @@ -0,0 +1,163 @@ + +from sys import stderr + +from dynamite import config +from dynamite.operators import sigmax, sigmay, sigmaz, index_sum +from dynamite.subspaces import SpinConserve +from dynamite.computations import entanglement_entropy +from dynamite.tools import mpi_print, MPI_COMM_WORLD + +import numpy as np +from argparse import ArgumentParser + + +def main(): + args = parse_args() + + # we print this to stderr to separate it from the data output below + mpi_print('== Run parameters: ==', file=stderr) + for key, value in vars(args).items(): + if key == 'seed': + continue # we handle seed below + mpi_print(f' {key}, {value}', file=stderr) + + # set a random seed, and output it for reproducibility + if args.seed is None: + seed = get_shared_seed() + else: + seed = args.seed + mpi_print(f' seed, {seed}', file=stderr) + np.random.seed(seed) + + # extra newline for readability of the output + mpi_print(file=stderr) + + # set spin chain length globally for dynamite + config.L = args.L + + # work in half-filling subspace + config.subspace = SpinConserve(args.L, args.L//2) + + # column headers + mpi_print('h,energy_point,entropy,ratio') + + for _ in range(args.iters): + for h in np.linspace(args.h_min, args.h_max, args.h_points): + H = build_hamiltonian(h, seed) + + # first solve for the exterior ones + + # by default eigsolve finds the lowest eigenpairs + evals, evecs = H.eigsolve(nev=args.nev, getvecs=True) + print_eig_stats(evals, evecs, h, energy_point=0) + min_eval = evals[0] + + # now the highest ones + evals, evecs = H.eigsolve(nev=args.nev, which='largest', getvecs=True) + print_eig_stats(evals, evecs, h, energy_point=1) + max_eval = evals[0] + + for energy_point in np.linspace(0, 1, args.energy_points)[1:-1]: + energy_target = min_eval + energy_point*(max_eval-min_eval) + evals, evecs = H.eigsolve(nev=args.nev, target=energy_target, getvecs=True) + print_eig_stats(evals, evecs, h=h, energy_point=energy_point) + + +def build_hamiltonian(h, seed=0xB0BACAFE): + ''' + Implements the nearest-neighbor Heisenberg interaction on a 1D spin chain, + plus random Z fields on each site. + ''' + + # 0.25 because we are working with Paulis and want spin operators + one_site_heisenberg = 0.25*sum(s(0)*s(1) for s in [sigmax, sigmay, sigmaz]) + full_chain_heisenberg = index_sum(one_site_heisenberg) + + # 0.5 again for Pauli -> spin operator conversion + random_fields = sum(0.5*np.random.uniform(-h, h)*sigmaz(i) for i in range(config.L)) + + return full_chain_heisenberg + random_fields + + +def print_eig_stats(evals, evecs, h, energy_point): + ''' + Compute the mean adjacent gap ratio and half-chain entanglement entropy + for the provided eigenvalues and eigenstates + ''' + # sum the entropy for all evecs then divide by nev for the mean + entropy = sum(entanglement_entropy(v, keep=range(config.L//2)) for v in evecs) + entropy /= len(evecs) + + # compute the adjacent gap ratio of the eigenvals + evals = sorted(evals) + ratio = 0 + for i in range(1, len(evals)-1): + this_gap = evals[i] - evals[i-1] + next_gap = evals[i+1] - evals[i] + ratio += min(this_gap, next_gap) / max(this_gap, next_gap) + ratio /= len(evals)-2 + + mpi_print(f'{h}, {energy_point}, {entropy}, {ratio}') + + +def get_shared_seed(): + ''' + Generate a seed for the random number generator, that is shared by all MPI ranks + ''' + from random import SystemRandom + + # get PETSc's MPI communicator object + comm = MPI_COMM_WORLD() + + # have rank 0 pick a seed + if comm.rank == 0: + # get a hardware-random number from the system to use as a seed + seed = SystemRandom().randrange(2**32) + else: + seed = None + + # if there is only one rank, don't need to do anything fancy + # doing this before using mpi4py below allows us to avoid needing mpi4py installed + # when we only use one rank + if comm.size == 1: + return seed + + # otherwise, we need to communicate the seed among the ranks, using mpi4py + # so we convert to a full-fledged mpi4py communicator class + comm = comm.tompi4py() + + # now broadcast from rank 0 to all other ranks + seed = comm.bcast(seed, root=0) + + return seed + + +def parse_args(): + ''' + Read arguments from the command line. + ''' + parser = ArgumentParser() + + parser.add_argument('-L', type=int, required=True, help='spin chain length') + parser.add_argument('--seed', type=int, + help='seed for random number generator. if omitted, a random ' + 'seed is chosen by querying system hardware randomness') + parser.add_argument('--iters', type=int, default=16, + help='number of disorder realizations') + + parser.add_argument('--energy-points', type=int, default=3, + help='number of points in the spectrum to target') + parser.add_argument('--h-points', type=int, default=5, + help='number of disorder strengths to test') + parser.add_argument('--h-min', type=float, default=1, + help='minimum value of disorder strength h') + parser.add_argument('--h-max', type=float, default=5, + help='maximum value of disorder strength h') + parser.add_argument('--nev', type=int, default=32, + help='number of eigenpairs to compute at each point') + + return parser.parse_args() + + +if __name__ == '__main__': + main() diff --git a/examples/scripts/README.md b/examples/scripts/README.md new file mode 100644 index 0000000..ef1fd9d --- /dev/null +++ b/examples/scripts/README.md @@ -0,0 +1,99 @@ + +# Example projects + +In this directory are several self-contained dynamite projects, which give examples of the kinds of problems dynamite is useful for and some best practices for writing code to solve them. + +Look around, and feel free to modify and explore. Each project has its own README which explains the idea and some interesting directions. You may, for example, want to try running some of these on a computer cluster and/or a GPU to see how that affects dynamite's performance! You should also feel free to use these scripts as a jumping off point for your own studies. + +## Features demonstrated + +Below we give (non-exhaustive) lists of the computations and programming patterns appearing in each of the examples, to help those looking to figure out how to perform particular tasks. If you don't see what you're looking for in these lists, we still suggest looking at the examples; it may just have not made the lists below! + +### In all examples + + - Building operators + - Using `mpi_print` to avoid duplicated output when running with many MPI ranks + - Separating "data" output and "human" output by printing to `stderr` (except Kagome example) + +### MBL + + - Eigensolving for ground states + - Eigensolving for states in the middle of the spectrum + - The `SpinConserve` subspace + - Computing entanglement entropy + - Coordinating randomness across MPI ranks + +### Kagome + + - Building Hamiltonians with arbitrary connectivity + - Eigensolving for ground states + - The `SpinConserve` subspace + - The `XParity` subspace + - Computing correlation functions + +### SYK + + - Mapping Majoranas onto spins + - Imaginary time evolution + - Ordinary time evolution + - Generating random states + - The `Parity` subspace + - Operators that cross subspaces (in this case, changing the parity) + - Out-of-time-order correlators + - Using quantum typicality to compute operators' thermal expectation values + - Coordinating randomness across MPI ranks + +### Floquet time evolution + + - Initializing product states + - Time evolution under piecewise Hamiltonians + - Computing entanglement entropy + - Computing expectation values + - Tracking each of the above throughout an evolution + - Checkpointing and restarting from a checkpoint + +## Running the scripts + +With a working dynamite installation, you can simply `cd` to the example you want to run and do + +```bash +python run_.py +``` +(for example, `run_mbl.py` for the MBL example). + +A key feature of dynamite is the ability to run scripts in parallel via MPI. On most systems you can run (using 4 MPI ranks for example): +```bash +mpirun -n 4 python run_.py +``` +On certain systems, like clusters, the command to launch MPI jobs may be different---check your system's documentation! + +You can also try running the examples on a GPU, if you have access to one! +If dynamite is compiled with GPU support it will perform computations on the GPU by default. The easiest way to access a dynamite build +that has been compiled with GPU support is probably via the Docker images; see [the documentation](https://dynamite.readthedocs.io/en/latest/containers.html) +for details! + +## Running in docker + +Note that these examples are included in dynamite's docker images at `/home/dnm/examples/scripts/`, so you can easily try them out. For example, to run the +Kagome example with 21 spins you can simply do + +```bash +docker run --rm -it gdmeyer/dynamite:latest python examples/kagome/run_kagome.py 21 +``` + +Or to run it [on a GPU in a compute cluster using Singularity](https://dynamite.readthedocs.io/en/latest/containers.html#singularity-usage): + +```bash +singularity exec --nv docker://gdmeyer/dynamite:latest-cuda python /home/dnm/examples/scripts/kagome/run_kagome.py 21 +``` + +## FAQ: the `main()` function + +You will notice each of the projects' run script has a `main()` function, and at the bottom has + +```python +if __name__ == '__main__': + main() +``` + +If you're not familiar with this, it causes `main()` to only run if the script is run directly (like via `python my_script.py`). This allows, for example, another Python script to do `import my_script` and then use functions from `my_script.py` without running the entire computation! This can be useful for testing and debugging. This structure also allows you to encapsulate variables into separate functions that achieve each step of the computation. Organizing your code like this is not at all required for dynamite to work, but we highly suggest it to make your development process easier and to avoid bugs! diff --git a/examples/scripts/SYK/README.ipynb b/examples/scripts/SYK/README.ipynb new file mode 100644 index 0000000..366042e --- /dev/null +++ b/examples/scripts/SYK/README.ipynb @@ -0,0 +1,396 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a9059c0-4c63-45be-8efe-15bfe7943db8", + "metadata": {}, + "source": [ + "# SYK\n", + "\n", + "## In this example\n", + "\n", + " - Mapping Majoranas onto spins\n", + " - Imaginary time evolution\n", + " - Ordinary time evolution\n", + " - Generating random states\n", + " - The `Parity` subspace\n", + " - Operators that cross subspaces (in this case, changing the parity)\n", + " - Out-of-time-order correlators\n", + " - Using quantum typicality to compute operators' thermal expectation values\n", + " - Coordinating randomness across MPI ranks" + ] + }, + { + "cell_type": "markdown", + "id": "1a9eb900-cdb8-43f4-851d-52a0d8dae5d0", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to exhibit *fast scrambling*, where quantum information is scrambled at the maximum possible rate. It is a particularly interesting system because it can be connected to the dynamics of quantum information in black holes, providing a testbed for surprising phenomena such as scrambling-based teleportation. The example code here mirrors closely a study which used dynamite to show numerical evidence of fast scrambling behavior in the SYK model.[1](#ref1)\n", + "TODO: I'd like to add several more references---is there a nice review paper that we could cite here?\n", + "\n", + "The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength:\n", + "\n", + "$$H = \\frac{6}{N^3} \\sum_{ijkl} J_{ijkl} \\chi_i \\chi_j \\chi_k \\chi_l$$\n", + "\n", + "where $J_{ijkl}$ are random with some particular distribution (we will use the uniform distribution in the range $[-1, 1]$).\n", + "\n", + "To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \\lfloor i/2 \\rfloor$. Then\n", + "$$\\chi_i = \\sigma^{\\{x, y\\}}_q \\prod_{m \\in [0, q-1]} \\sigma^z$$\n", + "where the first Pauli is $\\sigma^x$ if $i$ is even and $\\sigma^y$ if it's odd. In words, the Majorana consists of a $\\sigma^x$ or $\\sigma^y$ with a string of $\\sigma^z$ extending to the edge of the spin chain. Note that we get two Majoranas for each spin!\n", + "\n", + "This is straightforward to implement in dynamite, but is actually already built in in the `dynamite.extras` module so we don't have to do it ourselves:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "e87811a5-d53a-4602-aa1c-d0c3aedb37a1", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " coeff. | operator \n", + "=====================\n", + " 1.000 | ZZZZY\n" + ] + } + ], + "source": [ + "from dynamite.extras import majorana\n", + "\n", + "# a visual look at the Pauli structure of Majorana with index 9\n", + "print(majorana(9).table())" + ] + }, + { + "cell_type": "markdown", + "id": "ce55fb8f-7616-49c8-8470-38a11be906a5", + "metadata": {}, + "source": [ + "In this example project, the Hamiltonian is implemented by `build_hamiltonian` in the file `run_syk.py`. That function uses some clever optimizations to speed things up since our Hamiltonian has so many terms; we also include a more straightforward, but slower implementation called `build_hamiltonian_simple` for comparison. Check out the difference in performance, for a system of only 16 Majoranas (8 spins!):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "335563d7-757d-4597-a7e2-bc9fceb99e7f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from run_syk import build_hamiltonian, build_hamiltonian_simple" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f0b29c4d-53d1-4227-bd0b-952fd455af54", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "185 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit -n 1 -r 1 build_hamiltonian(N=16)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "de973a24-b451-4952-ba0b-7ff51330a9e8", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.99 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + ] + } + ], + "source": [ + "%timeit -n 1 -r 1 build_hamiltonian_simple(N=16)" + ] + }, + { + "cell_type": "markdown", + "id": "56149405-e6a5-4cbb-b21e-0e7520d58a0f", + "metadata": {}, + "source": [ + "## Goals\n", + "\n", + "In this project we investigate the fast scrambling behavior of the SYK model by studying *out of time order correlators* (OTOCs). In particular, we will measure how much a system is \"scrambled\" at time $t$ by measuring to what extent two local operators $V(0)$ and $W(t)$ anticommute (where the anticommutator at time $t=0$ is zero). The quantity we will measure is\n", + "$$C(t) = \\langle \\{ W(t), V(0) \\}^2 \\rangle .$$\n", + "It's helpful to reduce this to the following equivalent expression\n", + "$$C(t) = 2 \\mathrm{Re}\\left[ \\langle W(t) V(0) W(t) V(0) \\rangle \\right] + 1/2$$\n", + "which is the formulation of $C(t)$ that we will use in the computations here.\n", + "(For more details, see the referenced publications).\n", + "\n", + "We are specifically interested in the expectation value of the operator $O(t) = W(t) V(0) W(t) V(0)$ with respect to *thermal states* of various (inverse) temperatures $\\beta$. Now, dynamite's speed comes from the fact that it works with pure states, rather than mixed states---so the obvious plan to just compute $\\mathrm{Tr} \\left[ O(t) e^{-\\beta H} \\right]$ is out of the question. Instead, we can take advantage of an idea called *quantum typicality* to get an estimate of the expectation value more efficiently (see references below). Quantum typicality says that $\\mathrm{Tr} \\left[ O(t) e^{-\\beta H} \\right]$ is approximated by the expectation value of $O(t)$ with respect to random states of temperature $\\beta$. \n", + "\n", + "So, our plan will be to sample a number of random states, use imaginary time evolution to set their temperature to the given value of $\\beta$, and then take the expectation value of $O(t)$ with respect to the result. We will also take advantage of the fact that we can rewrite $\\mathrm{Tr} \\left[ O(t) e^{-\\beta H} \\right] = \\mathrm{Tr} \\left[ e^{-\\beta H/2} O(t) e^{-\\beta H/2} \\right]$. For simplicity we can set $W=\\chi_0$ and $V=\\chi_1$. With that, for a uniformly random state $\\left| \\psi_r \\right>$ we will compute (writing things out in full):\n", + "\n", + "$$\\left< \\psi_r \\right| e^{-\\beta H/2} e^{iHt} \\chi_0 e^{-iHt} \\chi_1 e^{iHt} \\chi_0 e^{-iHt} \\chi_1 e^{-\\beta H/2} \\left| \\psi_r \\right>$$\n", + "\n", + "The function `compute_otoc` starts with $\\left| \\psi_r \\right>$ and just works through the operator from right to left, applying imaginary and real time evolution and multiplying by operators as it goes until it finally reaches the other side, when it takes the inner product to find the expectation value." + ] + }, + { + "cell_type": "markdown", + "id": "3eaf6f16-c521-461e-8f00-d00ed2a1494a", + "metadata": {}, + "source": [ + "## Remark: matrix-free methods\n", + "\n", + "The SYK Hamiltonian has a very large number of terms. Most Hamiltonians we encounter have a number of terms that scale perhaps as $L$ or $L^2$, where $L$ is the number of spins. The SYK model has roughly $(2L)^4$ terms. For example, with only 40 Majoranas there are already over 90,000 terms!" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9df5924e-d62b-42ed-bcff-73d506e7c8bc", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "91390" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H = build_hamiltonian(40) # this make take a moment to run\n", + "H.nterms" + ] + }, + { + "cell_type": "markdown", + "id": "79d77970-e127-46b3-b0fc-bf62bda976c1", + "metadata": {}, + "source": [ + "Due to this fact, the memory required to store the matrix for this operator becomes very large:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a010990b-0c0e-49d8-a35c-c51ad3cb4ede", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "105.63354624" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H.estimate_memory() # returns an estimate in Gb of the memory required to build the matrix for this operator" + ] + }, + { + "cell_type": "markdown", + "id": "cfe237b5-d5d7-4532-9f3b-cac69e40508b", + "metadata": {}, + "source": [ + "Of note here is that `H` is a symbolic representation of the operator; the matrix has not been stored in memory. You don't need 105 Gb of RAM to evaluate the above two cells! One of the most important features of dynamite is the ability to perform computations \"matrix-free,\" such that the matrix is *never* built! Instead, matrix elements are generated on the fly when needed. For historical reasons, these are called \"shell\" matrices in dynamite. Using them results in a drastic reduction in memory usage:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "78557ef0-5494-4631-9b1a-958b57b3ecb4", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "0.00219336" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "H.shell = True\n", + "H.estimate_memory()" + ] + }, + { + "cell_type": "markdown", + "id": "9065a455-d109-403c-9475-68bd3a785afc", + "metadata": {}, + "source": [ + "In general, the tradeoff is that performing computations matrix-free can be somewhat slower, due to the extra cost of computing the matrix elements. But this is not always true: for matrix-vector multiplications, the limiting factor is often the memory bandwidth, and having to pull less data from memory can actually speed things up. This is especially true when running things in parallel, if many CPUs are sharing the same memory bus. Ultimately, one should just experiment with different configurations and see which one gives the best combination of speed and memory cost." + ] + }, + { + "cell_type": "markdown", + "id": "08b450d1-f16e-457f-88e3-3fcec4963a2a", + "metadata": {}, + "source": [ + "## Remark: disorder realizations and parallelism\n", + "\n", + "(also discussed in MBL example)\n", + "\n", + "The SYK Hamiltonian is a case in which getting good data requires disorder averaging---that is, running the computation many times with fresh randomness. Given $N$ CPU cores there are two main ways one can parallelize that process: (1) running $N$ disorder realizations independently at the same time, each using one core, and (2) using MPI to parallelize one computation across all $N$ cores and then doing each disorder realization in sequence. In this case, (1) will almost always be faster and should be prioritized---while the MPI parallelism in dynamite is highly optimized, there will always be some cost to the communication between cores.\n", + "\n", + "However, there are situations in which using MPI may be preferable, for example if running $N$ independent disorder realizations uses too much memory. Ultimately, the user should experiment with different configurations to determine what gives the best performance. Ideally, in practice one would simply make use of a large cluster of GPUs, running independent disorder realizations on each one." + ] + }, + { + "cell_type": "markdown", + "id": "a14821d4-0ad2-48b1-b659-93944958bcfa", + "metadata": {}, + "source": [ + "## Remark: using randomness in dynamite\n", + "\n", + "(also discussed in MBL example)\n", + "\n", + "One needs to take extra care when using randomness in code that will be run under MPI with multiple ranks. Each rank is its own Python process, and by default will have a different random seed---so if you are not careful, each of your ranks may end up trying to build a different Hamiltonian! (dynamite does check that the Hamiltonian is the same on all ranks before building the underlying matrix, so you will get an error if this happens).\n", + "\n", + "There are two ways to handle this: one is to have rank 0 pick a random seed and use MPI to communicate it to all the other ranks, and the other is to simply pass a seed on the command line. Both are implemented in this example for demonstration purposes: if no seed is passed on the command line, then one is generated and communicated to all MPI ranks. If you pass a random seed on the command line, make sure to change it each time you run the code if you want new disorder realizations!\n", + "\n", + "**Note 1:** when setting a random state via `State(state='random')`, dynamite is already careful about coordinating randomness between MPI ranks, so the user does not need to worry about it in that case.\n", + "\n", + "**Note 2:** If you will never run your code on multiple MPI ranks, you don't need to worry about this at all. In particular, running on a GPU with 1 CPU process will not encounter this issue." + ] + }, + { + "cell_type": "markdown", + "id": "e427b499-c8df-4448-9ea9-26dd46ffe1b7", + "metadata": {}, + "source": [ + "## Remark: re-using time evolution\n", + "\n", + "An important aspect of dynamite's time evolution algorithm is that its runtime is roughly proportional to $|| Ht ||$. In `run_syk.py`, we take advantage of this to reduce computational costs. Suppose we want to compute $C(t)$ for two temperatures $\\beta_1$ and $\\beta_2$, with $\\beta_1 < \\beta_2$. Starting with a random state $\\left| \\psi_r \\right>$, one might compute\n", + "\n", + "$$\\left| \\psi_{\\beta_1} \\right> = e^{-\\beta_1 H} \\left| \\psi_r \\right>$$\n", + "$$\\left| \\psi_{\\beta_2} \\right> = e^{-\\beta_2 H} \\left| \\psi_r \\right>$$\n", + "\n", + "However, it will be faster to compute\n", + "$$\\left| \\psi_{\\beta_1} \\right> = e^{-\\beta_1 H} \\left| \\psi_r \\right>$$\n", + "$$\\left| \\psi_{\\beta_2} \\right> = e^{-(\\beta_2-\\beta_1) H} \\left| \\psi_{\\beta_1} \\right>$$\n", + "because in this case, the exponent of the second evolution has smaller norm.\n", + "\n", + "Note that if one is computing the OTOC for multiple times $t$, one can take this a step further and do a similar thing for some of the time evolution operators $e^{-i H t}$ in the definition of the OTOC. (This is not implemented in this example)." + ] + }, + { + "cell_type": "markdown", + "id": "88a953ef-3e51-4fff-8550-6f4414c250ba", + "metadata": {}, + "source": [ + "## Usage\n", + "\n", + "The computation is implemented in `run_syk.py`. The script will output, in CSV format, the value of $C(t)$ for each combination of $\\beta$ and $t$ specified. Here are the command line options:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "aa8b3322-9bb0-4be2-b46a-65a429247328", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: run_syk.py [-h] [-N N] [-b B] [-t T] [--H-iters H_ITERS]\n", + " [--state-iters STATE_ITERS] [-s SEED] [--no-shell]\n", + "\n", + "Compute OTOCs for the SYK model.\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " -N N number of majoranas\n", + " -b B comma-separated list of values of beta\n", + " -t T comma-separated list of values of the time t\n", + " --H-iters H_ITERS number of Hamiltonian disorder realizations\n", + " --state-iters STATE_ITERS\n", + " number of random states per Hamiltonian\n", + " -s SEED, --seed SEED seed for random number generator. if omitted, a random\n", + " seed is chosen by querying system hardware randomness\n", + " --no-shell disable shell matrices (they are enabled by default)\n" + ] + } + ], + "source": [ + "! python run_syk.py -h" + ] + }, + { + "cell_type": "markdown", + "id": "d0170cd3-6f34-44d8-97a7-5e941a188bd9", + "metadata": {}, + "source": [ + "Try running this computation with MPI, or on a GPU if you have one, and compare the performance! You can also try disabling shell matrices with the `--no-shell` option to see first-hand how quickly the memory usage blows up for this Hamiltonian." + ] + }, + { + "cell_type": "markdown", + "id": "d6d79ea3-f797-408a-b0ab-7a4df02314d2", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1 [Kobrin et al., \"Many-Body Chaos in the Sachdev-Ye-Kitaev Model\"](https://doi.org/10.1103/PhysRevLett.126.030602) " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/scripts/SYK/README.md b/examples/scripts/SYK/README.md new file mode 100644 index 0000000..0fc461d --- /dev/null +++ b/examples/scripts/SYK/README.md @@ -0,0 +1,198 @@ +# SYK + +## In this example + + - Mapping Majoranas onto spins + - Imaginary time evolution + - Ordinary time evolution + - Generating random states + - The `Parity` subspace + - Operators that cross subspaces (in this case, changing the parity) + - Out-of-time-order correlators + - Using quantum typicality to compute operators' thermal expectation values + - Coordinating randomness across MPI ranks + +## Overview + +This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to exhibit *fast scrambling*, where quantum information is scrambled at the maximum possible rate. It is a particularly interesting system because it can be connected to the dynamics of quantum information in black holes, providing a testbed for surprising phenomena such as scrambling-based teleportation. The example code here mirrors closely a study which used dynamite to show numerical evidence of fast scrambling behavior in the SYK model.[1](#ref1) +TODO: I'd like to add several more references---is there a nice review paper that we could cite here? + +The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength: + +$$H = \frac{6}{N^3} \sum_{ijkl} J_{ijkl} \chi_i \chi_j \chi_k \chi_l$$ + +where $J_{ijkl}$ are random with some particular distribution (we will use the uniform distribution in the range $[-1, 1]$). + +To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \lfloor i/2 \rfloor$. Then +$$\chi_i = \sigma^{\{x, y\}}_q \prod_{m \in [0, q-1]} \sigma^z$$ +where the first Pauli is $\sigma^x$ if $i$ is even and $\sigma^y$ if it's odd. In words, the Majorana consists of a $\sigma^x$ or $\sigma^y$ with a string of $\sigma^z$ extending to the edge of the spin chain. Note that we get two Majoranas for each spin! + +This is straightforward to implement in dynamite, but is actually already built in in the `dynamite.extras` module so we don't have to do it ourselves: + + +```python +from dynamite.extras import majorana + +# a visual look at the Pauli structure of Majorana with index 9 +print(majorana(9).table()) +``` + + coeff. | operator + ===================== + 1.000 | ZZZZY + + +In this example project, the Hamiltonian is implemented by `build_hamiltonian` in the file `run_syk.py`. That function uses some clever optimizations to speed things up since our Hamiltonian has so many terms; we also include a more straightforward, but slower implementation called `build_hamiltonian_simple` for comparison. Check out the difference in performance, for a system of only 16 Majoranas (8 spins!): + + +```python +from run_syk import build_hamiltonian, build_hamiltonian_simple +``` + + +```python +%timeit -n 1 -r 1 build_hamiltonian(N=16) +``` + + 185 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) + + + +```python +%timeit -n 1 -r 1 build_hamiltonian_simple(N=16) +``` + + 1.99 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) + + +## Goals + +In this project we investigate the fast scrambling behavior of the SYK model by studying *out of time order correlators* (OTOCs). In particular, we will measure how much a system is "scrambled" at time $t$ by measuring to what extent two local operators $V(0)$ and $W(t)$ anticommute (where the anticommutator at time $t=0$ is zero). The quantity we will measure is +$$C(t) = \langle \{ W(t), V(0) \}^2 \rangle .$$ +It's helpful to reduce this to the following equivalent expression +$$C(t) = 2 \mathrm{Re}\left[ \langle W(t) V(0) W(t) V(0) \rangle \right] + 1/2$$ +which is the formulation of $C(t)$ that we will use in the computations here. +(For more details, see the referenced publications). + +We are specifically interested in the expectation value of the operator $O(t) = W(t) V(0) W(t) V(0)$ with respect to *thermal states* of various (inverse) temperatures $\beta$. Now, dynamite's speed comes from the fact that it works with pure states, rather than mixed states---so the obvious plan to just compute $\mathrm{Tr} \left[ O(t) e^{-\beta H} \right]$ is out of the question. Instead, we can take advantage of an idea called *quantum typicality* to get an estimate of the expectation value more efficiently (see references below). Quantum typicality says that $\mathrm{Tr} \left[ O(t) e^{-\beta H} \right]$ is approximated by the expectation value of $O(t)$ with respect to random states of temperature $\beta$. + +So, our plan will be to sample a number of random states, use imaginary time evolution to set their temperature to the given value of $\beta$, and then take the expectation value of $O(t)$ with respect to the result. We will also take advantage of the fact that we can rewrite $\mathrm{Tr} \left[ O(t) e^{-\beta H} \right] = \mathrm{Tr} \left[ e^{-\beta H/2} O(t) e^{-\beta H/2} \right]$. For simplicity we can set $W=\chi_0$ and $V=\chi_1$. With that, for a uniformly random state $\left| \psi_r \right>$ we will compute (writing things out in full): + +$$\left< \psi_r \right| e^{-\beta H/2} e^{iHt} \chi_0 e^{-iHt} \chi_1 e^{iHt} \chi_0 e^{-iHt} \chi_1 e^{-\beta H/2} \left| \psi_r \right>$$ + +The function `compute_otoc` starts with $\left| \psi_r \right>$ and just works through the operator from right to left, applying imaginary and real time evolution and multiplying by operators as it goes until it finally reaches the other side, when it takes the inner product to find the expectation value. + +## Remark: matrix-free methods + +The SYK Hamiltonian has a very large number of terms. Most Hamiltonians we encounter have a number of terms that scale perhaps as $L$ or $L^2$, where $L$ is the number of spins. The SYK model has roughly $(2L)^4$ terms. For example, with only 40 Majoranas there are already over 90,000 terms! + + +```python +H = build_hamiltonian(40) # this make take a moment to run +H.nterms +``` + + + + + 91390 + + + +Due to this fact, the memory required to store the matrix for this operator becomes very large: + + +```python +H.estimate_memory() # returns an estimate in Gb of the memory required to build the matrix for this operator +``` + + + + + 105.63354624 + + + +Of note here is that `H` is a symbolic representation of the operator; the matrix has not been stored in memory. You don't need 105 Gb of RAM to evaluate the above two cells! One of the most important features of dynamite is the ability to perform computations "matrix-free," such that the matrix is *never* built! Instead, matrix elements are generated on the fly when needed. For historical reasons, these are called "shell" matrices in dynamite. Using them results in a drastic reduction in memory usage: + + +```python +H.shell = True +H.estimate_memory() +``` + + + + + 0.00219336 + + + +In general, the tradeoff is that performing computations matrix-free can be somewhat slower, due to the extra cost of computing the matrix elements. But this is not always true: for matrix-vector multiplications, the limiting factor is often the memory bandwidth, and having to pull less data from memory can actually speed things up. This is especially true when running things in parallel, if many CPUs are sharing the same memory bus. Ultimately, one should just experiment with different configurations and see which one gives the best combination of speed and memory cost. + +## Remark: disorder realizations and parallelism + +(also discussed in MBL example) + +The SYK Hamiltonian is a case in which getting good data requires disorder averaging---that is, running the computation many times with fresh randomness. Given $N$ CPU cores there are two main ways one can parallelize that process: (1) running $N$ disorder realizations independently at the same time, each using one core, and (2) using MPI to parallelize one computation across all $N$ cores and then doing each disorder realization in sequence. In this case, (1) will almost always be faster and should be prioritized---while the MPI parallelism in dynamite is highly optimized, there will always be some cost to the communication between cores. + +However, there are situations in which using MPI may be preferable, for example if running $N$ independent disorder realizations uses too much memory. Ultimately, the user should experiment with different configurations to determine what gives the best performance. Ideally, in practice one would simply make use of a large cluster of GPUs, running independent disorder realizations on each one. + +## Remark: using randomness in dynamite + +(also discussed in MBL example) + +One needs to take extra care when using randomness in code that will be run under MPI with multiple ranks. Each rank is its own Python process, and by default will have a different random seed---so if you are not careful, each of your ranks may end up trying to build a different Hamiltonian! (dynamite does check that the Hamiltonian is the same on all ranks before building the underlying matrix, so you will get an error if this happens). + +There are two ways to handle this: one is to have rank 0 pick a random seed and use MPI to communicate it to all the other ranks, and the other is to simply pass a seed on the command line. Both are implemented in this example for demonstration purposes: if no seed is passed on the command line, then one is generated and communicated to all MPI ranks. If you pass a random seed on the command line, make sure to change it each time you run the code if you want new disorder realizations! + +**Note 1:** when setting a random state via `State(state='random')`, dynamite is already careful about coordinating randomness between MPI ranks, so the user does not need to worry about it in that case. + +**Note 2:** If you will never run your code on multiple MPI ranks, you don't need to worry about this at all. In particular, running on a GPU with 1 CPU process will not encounter this issue. + +## Remark: re-using time evolution + +An important aspect of dynamite's time evolution algorithm is that its runtime is roughly proportional to $|| Ht ||$. In `run_syk.py`, we take advantage of this to reduce computational costs. Suppose we want to compute $C(t)$ for two temperatures $\beta_1$ and $\beta_2$, with $\beta_1 < \beta_2$. Starting with a random state $\left| \psi_r \right>$, one might compute + +$$\left| \psi_{\beta_1} \right> = e^{-\beta_1 H} \left| \psi_r \right>$$ +$$\left| \psi_{\beta_2} \right> = e^{-\beta_2 H} \left| \psi_r \right>$$ + +However, it will be faster to compute +$$\left| \psi_{\beta_1} \right> = e^{-\beta_1 H} \left| \psi_r \right>$$ +$$\left| \psi_{\beta_2} \right> = e^{-(\beta_2-\beta_1) H} \left| \psi_{\beta_1} \right>$$ +because in this case, the exponent of the second evolution has smaller norm. + +Note that if one is computing the OTOC for multiple times $t$, one can take this a step further and do a similar thing for some of the time evolution operators $e^{-i H t}$ in the definition of the OTOC. (This is not implemented in this example). + +## Usage + +The computation is implemented in `run_syk.py`. The script will output, in CSV format, the value of $C(t)$ for each combination of $\beta$ and $t$ specified. Here are the command line options: + + +```python +! python run_syk.py -h +``` + + usage: run_syk.py [-h] [-N N] [-b B] [-t T] [--H-iters H_ITERS] + [--state-iters STATE_ITERS] [-s SEED] [--no-shell] + + Compute OTOCs for the SYK model. + + options: + -h, --help show this help message and exit + -N N number of majoranas + -b B comma-separated list of values of beta + -t T comma-separated list of values of the time t + --H-iters H_ITERS number of Hamiltonian disorder realizations + --state-iters STATE_ITERS + number of random states per Hamiltonian + -s SEED, --seed SEED seed for random number generator. if omitted, a random + seed is chosen by querying system hardware randomness + --no-shell disable shell matrices (they are enabled by default) + + +Try running this computation with MPI, or on a GPU if you have one, and compare the performance! You can also try disabling shell matrices with the `--no-shell` option to see first-hand how quickly the memory usage blows up for this Hamiltonian. + +## References + +1 [Kobrin et al., "Many-Body Chaos in the Sachdev-Ye-Kitaev Model"](https://doi.org/10.1103/PhysRevLett.126.030602) diff --git a/examples/scripts/SYK/run_syk.py b/examples/scripts/SYK/run_syk.py new file mode 100644 index 0000000..43bae14 --- /dev/null +++ b/examples/scripts/SYK/run_syk.py @@ -0,0 +1,271 @@ + +from itertools import combinations +from argparse import ArgumentParser +from sys import stderr +from numpy import random + +from dynamite import config +from dynamite.operators import op_sum, op_product +from dynamite.extras import majorana +from dynamite.subspaces import Parity +from dynamite.states import State +from dynamite.tools import mpi_print, MPI_COMM_WORLD + + +def main(): + args = parse_args() + + # we print this to stderr to separate it from the data output below + mpi_print('== Run parameters: ==', file=stderr) + for key, value in vars(args).items(): + if key == 'seed': + continue # we handle seed below + mpi_print(f' {key}, {value}', file=stderr) + + # set a random seed, and output it for reproducibility + if args.seed is None: + seed = get_shared_seed() + else: + seed = args.seed + mpi_print(f' seed, {seed}', file=stderr) + random.seed(seed) + + # extra newline for readability of the output + mpi_print(file=stderr) + + # globally enable shell matrices (unless command line told us not to) + config.shell = not args.no_shell + + # globally set the number of spins to ceil(N/2) + config.L = (args.N+1)//2 + + # ensures we get the same random numbers on all MPI ranks + random.seed(args.seed) + + # the Hamiltonian conserves spin parity in the Z basis (Z2 symmetry) + # but the majorana operators W and V take us between the two symmetry sectors + # so we will make use of both parity subspaces (see below) + even_space = Parity('even') + odd_space = Parity('odd') + + W = majorana(0) + V = majorana(1) + + # specify that these operators take odd to even and even to odd + W.add_subspace(even_space, odd_space) + W.add_subspace(odd_space, even_space) + + V.add_subspace(even_space, odd_space) + V.add_subspace(odd_space, even_space) + + sorted_beta = sorted(args.b) + + # print the headers for the output CSV + mpi_print("beta,t,C") + + for _ in range(args.H_iters): # disorder realizations + + H = build_hamiltonian(args.N) + + # H conserves parity, and will operate on both odd and even spaces at various points + # so we add both of them. which subspace is used will be automatically chosen based on the + # state that H is applied to. + # when only one argument is supplied to add_subspace, it is used for both the + # "left" and "right" subspace (implying it is conserved) + H.add_subspace(even_space) + H.add_subspace(odd_space) + + for _ in range(args.state_iters): + # we'll have psi start on the even subspace + psi0 = State(state='random', subspace=even_space) + psi1 = psi0.copy() # a place to put the evolution result + + for i, b in enumerate(sorted_beta): + # cost of time evolution is proportional to || bH || + # so, we can save some time by starting our imaginary time evolution at the previous + # beta (note that the results will be correlated across beta values because we are + # re-using psi, as long as we are careful in our analysis and do enough disorder + # realizations this is OK) + if i == 0: + delta_b = b + else: + delta_b = b - sorted_beta[i-1] + + # do imaginary time evolution to compute e^{-beta/2 H} |psi> + # we will take the expectation value of the OTOC with respect to the result + H.evolve(psi0, t=-1j*delta_b, result=psi1) + psi1.normalize() + + # set psi0 to equal psi1 + psi1.copy(result=psi0) + + # compute_otoc will not touch psi1 (but it does modify psi0) + for t in args.t: + result = compute_otoc(psi0, psi1, t, H, W, V) + + mpi_print(f"{b},{t},{result}") + + # restore psi0 to equal psi1 for the next iteration + psi1.copy(result=psi0) + + +def build_hamiltonian_simple(N): + ''' + This function is the most straightforward way to generate the + Hamiltonian, but it's not nearly as fast as build_hamiltonian + below. We still include it as an example. + ''' + H = 0 + for i in range(N): + for j in range(i+1, N): + for k in range(j+1, N): + for l in range(k+1, N): + Jijkl = random.uniform(-1, 1) + H += Jijkl*majorana(i)*majorana(j)*majorana(k)*majorana(l) + + return 6/N**3 * H + + +def build_hamiltonian(N): + ''' + This function builds the SYK Hamiltonian, and is about 10 times faster + than build_hamiltonian_simple + ''' + + # pre-compute all the majorana operators because we will re-use them many times + majoranas = [majorana(i) for i in range(N)] + + # This is a Python generator, which produces the terms of + # the Hamiltonian + def gen_products(N): + for idxs in combinations(range(N), 4): + # computes the product of the four majoranas + # faster than using several "*" operations because it does not create a new + # intermediate operator object with each multiplication + p = op_product(majoranas[idx] for idx in idxs) + + # random value is the same on each rank because we set the seed explicitly in main() + Jijkl = random.uniform(-1, 1) + + # using scale() is faster than doing 'Jijkl*p' because it does not create + # a new operator object, instead just scaling the coeffs of the existing one + p.scale(Jijkl) + + yield p + + # op_sum iterates through the terms generated by gen_products, and sums + # them more efficiently than doing 'H += term' for each term + H = op_sum(gen_products(N)) + + # global prefactor + # again, using scale() is much more efficient than "return 6/N**3 * H" + # because it doesn't create a copy of the whole gigantic Hamiltonian + H.scale(6/N**3) + + return H + + +def compute_otoc(psi0, psi1, t, H, W, V): + ''' + Computes the value + C = 2*real() + 0.5 + where W(t) = e^{iHt} W e^{-iHt} + + the contents of psi1 will not be modified; but psi0 will + ''' + + # apply V, allocating a new vector + # ideally one would reuse the same vector across disorder realizations, but + # the cost of reallocating this vector once per iteration is negligible compared + # to the rest of the computation (and the memory gets freed each iteration when + # the variable goes out of scope) + tmp_odd_0 = V*psi0 # note that tmp_odd_0 is in the "odd" subspace + + # next up is e^{-iHt} in the definition of W(t) + # here we are implicitly allocating another vector in the odd subspace + tmp_odd_1 = H.evolve(tmp_odd_0, t=t) + + # apply W, taking us back into the even subspace + # we can reuse psi0 here to save some memory + W.dot(tmp_odd_1, result=psi0) + + # apply the e^{iHt} on the other side of W(t) + tmp_even = H.evolve(psi0, t=-t) + + # now V takes us back to the odd subspace again + # we can reuse tmp_odd_0 + V.dot(tmp_even, result=tmp_odd_0) + + # now e^{-iHt} on the right of our final W(t) + H.evolve(tmp_odd_0, t=t, result=tmp_odd_1) + + # finally back to the even subspace for the last time + W.dot(tmp_odd_1, result=psi0) + + # and the final (reverse) time evolution on the left of the last W(t) + H.evolve(psi0, t=-t, result=tmp_even) + + # at last we take the inner product with psi1 + result = psi1.dot(tmp_even) + + # this is the value C(t) that we really care about---see README + return 2*result.real + 0.5 + + +def get_shared_seed(): + ''' + Generate a seed for the random number generator, that is shared by all MPI ranks + ''' + from random import SystemRandom + + # get PETSc's MPI communicator object + comm = MPI_COMM_WORLD() + + # have rank 0 pick a seed + if comm.rank == 0: + # get a hardware-random number from the system to use as a seed + seed = SystemRandom().randrange(2**32) + else: + seed = None + + # if there is only one rank, don't need to do anything fancy + # doing this before using mpi4py below allows us to avoid needing mpi4py installed + # when we only use one rank + if comm.size == 1: + return seed + + # otherwise, we need to communicate the seed among the ranks, using mpi4py + # so we convert to a full-fledged mpi4py communicator class + comm = comm.tompi4py() + + # now broadcast from rank 0 to all other ranks + seed = comm.bcast(seed, root=0) + + return seed + + +def parse_args(): + parser = ArgumentParser(description='Compute OTOCs for the SYK model.') + + parser.add_argument('-N', default=30, type=int, help='number of majoranas') + parser.add_argument('-b', default=[0.5], type=lambda s: [float(x) for x in s.split(',')], + help='comma-separated list of values of beta') + parser.add_argument('-t', default=[0.5], type=lambda s: [float(x) for x in s.split(',')], + help='comma-separated list of values of the time t') + parser.add_argument('--H-iters', default=1, type=int, + help='number of Hamiltonian disorder realizations') + parser.add_argument('--state-iters', default=1, type=int, + help='number of random states per Hamiltonian') + + parser.add_argument('-s', '--seed', type=int, + help='seed for random number generator. if omitted, a random ' + 'seed is chosen by querying system hardware randomness') + + parser.add_argument('--no-shell', action='store_true', + help='disable shell matrices (they are enabled by default)') + + return parser.parse_args() + + +if __name__ == '__main__': + main() diff --git a/examples/scripts/floquet/README.ipynb b/examples/scripts/floquet/README.ipynb new file mode 100644 index 0000000..099d380 --- /dev/null +++ b/examples/scripts/floquet/README.ipynb @@ -0,0 +1,234 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "25f5176c-7180-4024-9f1f-83c30bd28e39", + "metadata": {}, + "source": [ + "# Floquet\n", + "\n", + "## In this example\n", + "\n", + " - Initializing product states\n", + " - Time evolution under piecewise Hamiltonians\n", + " - Computing entanglement entropy\n", + " - Computing expectation values\n", + " - Tracking each of the above throughout an evolution\n", + " - Checkpointing and restarting from a checkpoint\n", + " \n", + "## Overview\n", + "\n", + "In this project we will track the time evolution of various states under a time-dependent Floquet Hamiltonian. The quantum system we analyze is physically interesting for a number of reasons, not least of which that it can exhibit Floquet prethermalization,[1](#ref1) which can support out-of-equilibrium phases of matter like time crystals! [2](#ref2)\n", + "\n", + "The specific model we will implement is the following. The 1D spin chain will evolve under a long range $ZZ$ interaction decaying as a power law, along with a nearest-neighbor $XX$ interaction and a uniform, static magnetic field $\\vec{h}$:\n", + "$$H = J_z \\sum_{i,j} \\frac{\\sigma^z_i \\sigma^z_j}{|i-j|^\\alpha} + J_x \\sum_{\\langle i, j \\rangle} \\sigma^x_i + \\sum_i \\vec{h} \\cdot \\vec{\\sigma}$$\n", + "where the angle brackets on the second term indicates it is only a nearest-neighbor interaction.\n", + "\n", + "In addition, after every period $T$ of time evolution the system will undergo a global $\\pi$-pulse, rotating all spins by $180^\\circ$ around the $X$-axis. (We can equivalently think of this as flipping the direction of the magnetic field $\\vec{h}$ across the $X$ axis every time $T$).\n", + "\n", + "The Hamiltonian $H$ is implemented in `build_hamiltonian` in `run_floquet.py`:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "372654a2-1faf-4f35-8384-77269ac594da", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\sum\\limits_{i=0}^{10}0.25\\sigma^z_{i}\\sigma^z_{i+1} + 0.42\\left[\\sum\\limits_{i=0}^{9}0.25\\sigma^z_{i}\\sigma^z_{i+2}\\right] + 0.253\\left[\\sum\\limits_{i=0}^{8}0.25\\sigma^z_{i}\\sigma^z_{i+3}\\right] + \\cdots + 0.2\\left[\\sum\\limits_{i=0}^{10}0.25\\sigma^x_{i}\\sigma^x_{i+1}\\right] + \\sum\\limits_{i=0}^{11}\\left(0.1\\sigma^x_{i} + 0.075\\sigma^y_{i} + 0.05\\sigma^z_{i}\\right)$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from run_floquet import build_hamiltonian\n", + "\n", + "# for this one its easiest to just globally set L\n", + "from dynamite import config\n", + "config.L = 12\n", + "\n", + "build_hamiltonian(1.25, 1, 0.2, (0.2, 0.15, 0.1))" + ] + }, + { + "cell_type": "markdown", + "id": "8515ea15-9dbe-4df0-ae29-14f9e683f8d7", + "metadata": {}, + "source": [ + "and the global pi-pulse is simply a multiplication by the all-Pauli-$X$ string:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1ee76dfa-b3ab-4e4a-8605-90a9ad3630ee", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/latex": [ + "$\\prod\\limits_{i=0}^{11}\\sigma^x_{i}$" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from dynamite.operators import sigmax, index_product\n", + "\n", + "pi_pulse = index_product(sigmax())\n", + "pi_pulse" + ] + }, + { + "cell_type": "markdown", + "id": "2fa5743f-e14c-4d51-8798-c3baee7d6896", + "metadata": {}, + "source": [ + "## Goals\n", + "\n", + "The actual numerics here are pretty straightforward: we will evolve an initial state under the above Floquet model, and at every time $T$ we will compute various statistics about the state, such as expectation values of operators and the entanglement entropy. We will also observe the \"energy\" of the state with respect to the Hamiltonian averaged over a full cycle of length $2T$:\n", + "$$D_\\mathrm{eff} = J_z \\sum_{i,j} \\frac{\\sigma^z_i \\sigma^z_j}{|i-j|^\\alpha} + J_x \\sum_{\\langle i, j \\rangle} \\sigma^x_i + \\sum_i h_x \\sigma_x$$\n", + "That is, the $h_y$ and $h_z$ terms approximately average to zero. \n", + "\n", + "Of course, since we are driving the system and there is no dissipation, the temperature will eventually go to infinity. However, we hope to observe that for the right frequency (inverse of the period $T$) we should see a \"prethermal plateau\" in which the system first thermalizes with respect to the $D_\\mathrm{eff}$, but the expectation value $\\langle D_\\mathrm{eff} \\rangle$ is approximately conserved. This is *Floquet prethermalization*---the system thermalizes early with respect to an approximate Hamiltonian, and only much later thermalizes to the infinite temperature state." + ] + }, + { + "cell_type": "markdown", + "id": "02058a72-087a-4add-8476-7c8160b0ca4d", + "metadata": {}, + "source": [ + "## Remark: checkpointing\n", + "\n", + "Especially on HPC clusters, we may encounter situations where our compute jobs are killed before they complete. Or, we may look at the results of a completed job and decide we want to evolve for a longer time. Whatever the reason, the ability to save our progress and re-start computations where we left off can be extremely useful.\n", + "\n", + "This can often be quite easy to accomplish, and it is implemented in this example. Here, every $n$ iterations (where $n$ is set on the command line by the user), the state vector is saved to a file, with the cycle number contained in the filename. When the script starts up, it checks to see if such a file already exists, and if it does, it reads in the vector and starts the evolution from there. Pretty straightforward!" + ] + }, + { + "cell_type": "markdown", + "id": "76458b0a-d329-48e8-a856-d4b5ed015f0e", + "metadata": {}, + "source": [ + "## Remark: operator arithmetic\n", + "\n", + "Note that in `run_floquet.py` we compute the \"averaged\" Hamiltonian $D_\\mathrm{eff}$ simply as\n", + "\n", + "```python\n", + "Deff = (H + X*H*X)/2\n", + "```\n", + "where `X` is the global pi pulse operator.\n", + "\n", + "This showcases the operator arithmetic that is possible in dynamite. You may sometimes find dynamite useful just as a \"calculator\" for doing arithmetic with strings of Paulis! Note in particular that due to the symbolic way that dynamite stores operators, calculations like the above can be done in milliseconds on a laptop even when multiplying the operators as matrices would be extremely expensive." + ] + }, + { + "cell_type": "markdown", + "id": "d3a2fff1-e98e-4daf-8234-f8864cd96b4d", + "metadata": {}, + "source": [ + "## Usage\n", + "\n", + "The computation is implemented in `run_floquet.py`. The script will output, in CSV format, the half-chain entanglement entropy, the effective energy $\\langle D_\\mathrm{eff} \\rangle$, and the expectation value of $S^z$ for each spin. Note that the data is written to stdout and any other information is written to stderr, so you can do for example `python run_floquet.py -L 12 > data.csv` and only the data will be written to the CSV file.\n", + "\n", + "Here are the command line options:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "20f3ae00-497c-4428-b2ef-e7d76d39afe2", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: run_floquet.py [-h] [-L L] [--Jx JX] [--h-vec H_VEC] [--alpha ALPHA]\n", + " [-T T] [--initial-state-dwalls INITIAL_STATE_DWALLS]\n", + " [--n-cycles N_CYCLES]\n", + " [--checkpoint-path CHECKPOINT_PATH]\n", + " [--checkpoint-every CHECKPOINT_EVERY]\n", + "\n", + "Evolve under a Floquet Hamiltonian\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " -L L number of spins\n", + " --Jx JX coefficient on the XX term\n", + " --h-vec H_VEC magnetic field vector\n", + " --alpha ALPHA power law for long range ZZ interaction\n", + " -T T Floquet period\n", + " --initial-state-dwalls INITIAL_STATE_DWALLS\n", + " Number of domain walls to include in initial product\n", + " state\n", + " --n-cycles N_CYCLES Total number of Floquet cycles\n", + " --checkpoint-path CHECKPOINT_PATH\n", + " where to save the state vector for\n", + " checkpointing/restarting. [default: ./]\n", + " --checkpoint-every CHECKPOINT_EVERY\n", + " how frequently to save checkpoints, in number of\n", + " cycles. if this option is omitted, checkpoints will\n", + " not be saved.\n" + ] + } + ], + "source": [ + "! python run_floquet.py -h" + ] + }, + { + "cell_type": "markdown", + "id": "6c01d1a9-88c2-4ba1-8f0c-e0e13fa8ec00", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1 [Machado et al., \"Exponentially slow heating in short and long-range interacting Floquet systems\"](https://doi.org/10.1103/PhysRevResearch.1.033202) \n", + "2 [Machado et al., \"Long-Range Prethermal Phases of Nonequilibrium Matter\"](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.10.011043) " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/scripts/floquet/README.md b/examples/scripts/floquet/README.md new file mode 100644 index 0000000..a4b655b --- /dev/null +++ b/examples/scripts/floquet/README.md @@ -0,0 +1,126 @@ +# Floquet + +## In this example + + - Initializing product states + - Time evolution under piecewise Hamiltonians + - Computing entanglement entropy + - Computing expectation values + - Tracking each of the above throughout an evolution + - Checkpointing and restarting from a checkpoint + +## Overview + +In this project we will track the time evolution of various states under a time-dependent Floquet Hamiltonian. The quantum system we analyze is physically interesting for a number of reasons, not least of which that it can exhibit Floquet prethermalization,[1](#ref1) which can support out-of-equilibrium phases of matter like time crystals! [2](#ref2) + +The specific model we will implement is the following. The 1D spin chain will evolve under a long range $ZZ$ interaction decaying as a power law, along with a nearest-neighbor $XX$ interaction and a uniform, static magnetic field $\vec{h}$: +$$H = J_z \sum_{i,j} \frac{\sigma^z_i \sigma^z_j}{|i-j|^\alpha} + J_x \sum_{\langle i, j \rangle} \sigma^x_i + \sum_i \vec{h} \cdot \vec{\sigma}$$ +where the angle brackets on the second term indicates it is only a nearest-neighbor interaction. + +In addition, after every period $T$ of time evolution the system will undergo a global $\pi$-pulse, rotating all spins by $180^\circ$ around the $X$-axis. (We can equivalently think of this as flipping the direction of the magnetic field $\vec{h}$ across the $X$ axis every time $T$). + +The Hamiltonian $H$ is implemented in `build_hamiltonian` in `run_floquet.py`: + + +```python +from run_floquet import build_hamiltonian + +# for this one its easiest to just globally set L +from dynamite import config +config.L = 12 + +build_hamiltonian(1.25, 1, 0.2, (0.2, 0.15, 0.1)) +``` + + + + +$\sum\limits_{i=0}^{10}0.25\sigma^z_{i}\sigma^z_{i+1} + 0.42\left[\sum\limits_{i=0}^{9}0.25\sigma^z_{i}\sigma^z_{i+2}\right] + 0.253\left[\sum\limits_{i=0}^{8}0.25\sigma^z_{i}\sigma^z_{i+3}\right] + \cdots + 0.2\left[\sum\limits_{i=0}^{10}0.25\sigma^x_{i}\sigma^x_{i+1}\right] + \sum\limits_{i=0}^{11}\left(0.1\sigma^x_{i} + 0.075\sigma^y_{i} + 0.05\sigma^z_{i}\right)$ + + + +and the global pi-pulse is simply a multiplication by the all-Pauli-$X$ string: + + +```python +from dynamite.operators import sigmax, index_product + +pi_pulse = index_product(sigmax()) +pi_pulse +``` + + + + +$\prod\limits_{i=0}^{11}\sigma^x_{i}$ + + + +## Goals + +The actual numerics here are pretty straightforward: we will evolve an initial state under the above Floquet model, and at every time $T$ we will compute various statistics about the state, such as expectation values of operators and the entanglement entropy. We will also observe the "energy" of the state with respect to the Hamiltonian averaged over a full cycle of length $2T$: +$$D_\mathrm{eff} = J_z \sum_{i,j} \frac{\sigma^z_i \sigma^z_j}{|i-j|^\alpha} + J_x \sum_{\langle i, j \rangle} \sigma^x_i + \sum_i h_x \sigma_x$$ +That is, the $h_y$ and $h_z$ terms approximately average to zero. + +Of course, since we are driving the system and there is no dissipation, the temperature will eventually go to infinity. However, we hope to observe that for the right frequency (inverse of the period $T$) we should see a "prethermal plateau" in which the system first thermalizes with respect to the $D_\mathrm{eff}$, but the expectation value $\langle D_\mathrm{eff} \rangle$ is approximately conserved. This is *Floquet prethermalization*---the system thermalizes early with respect to an approximate Hamiltonian, and only much later thermalizes to the infinite temperature state. + +## Remark: checkpointing + +Especially on HPC clusters, we may encounter situations where our compute jobs are killed before they complete. Or, we may look at the results of a completed job and decide we want to evolve for a longer time. Whatever the reason, the ability to save our progress and re-start computations where we left off can be extremely useful. + +This can often be quite easy to accomplish, and it is implemented in this example. Here, every $n$ iterations (where $n$ is set on the command line by the user), the state vector is saved to a file, with the cycle number contained in the filename. When the script starts up, it checks to see if such a file already exists, and if it does, it reads in the vector and starts the evolution from there. Pretty straightforward! + +## Remark: operator arithmetic + +Note that in `run_floquet.py` we compute the "averaged" Hamiltonian $D_\mathrm{eff}$ simply as + +```python +Deff = (H + X*H*X)/2 +``` +where `X` is the global pi pulse operator. + +This showcases the operator arithmetic that is possible in dynamite. You may sometimes find dynamite useful just as a "calculator" for doing arithmetic with strings of Paulis! Note in particular that due to the symbolic way that dynamite stores operators, calculations like the above can be done in milliseconds on a laptop even when multiplying the operators as matrices would be extremely expensive. + +## Usage + +The computation is implemented in `run_floquet.py`. The script will output, in CSV format, the half-chain entanglement entropy, the effective energy $\langle D_\mathrm{eff} \rangle$, and the expectation value of $S^z$ for each spin. Note that the data is written to stdout and any other information is written to stderr, so you can do for example `python run_floquet.py -L 12 > data.csv` and only the data will be written to the CSV file. + +Here are the command line options: + + +```python +! python run_floquet.py -h +``` + + usage: run_floquet.py [-h] [-L L] [--Jx JX] [--h-vec H_VEC] [--alpha ALPHA] + [-T T] [--initial-state-dwalls INITIAL_STATE_DWALLS] + [--n-cycles N_CYCLES] + [--checkpoint-path CHECKPOINT_PATH] + [--checkpoint-every CHECKPOINT_EVERY] + + Evolve under a Floquet Hamiltonian + + options: + -h, --help show this help message and exit + -L L number of spins + --Jx JX coefficient on the XX term + --h-vec H_VEC magnetic field vector + --alpha ALPHA power law for long range ZZ interaction + -T T Floquet period + --initial-state-dwalls INITIAL_STATE_DWALLS + Number of domain walls to include in initial product + state + --n-cycles N_CYCLES Total number of Floquet cycles + --checkpoint-path CHECKPOINT_PATH + where to save the state vector for + checkpointing/restarting. [default: ./] + --checkpoint-every CHECKPOINT_EVERY + how frequently to save checkpoints, in number of + cycles. if this option is omitted, checkpoints will + not be saved. + + +## References + +1 [Machado et al., "Exponentially slow heating in short and long-range interacting Floquet systems"](https://doi.org/10.1103/PhysRevResearch.1.033202) +2 [Machado et al., "Long-Range Prethermal Phases of Nonequilibrium Matter"](https://journals.aps.org/prx/abstract/10.1103/PhysRevX.10.011043) diff --git a/examples/scripts/floquet/run_floquet.py b/examples/scripts/floquet/run_floquet.py new file mode 100644 index 0000000..6a0ed0e --- /dev/null +++ b/examples/scripts/floquet/run_floquet.py @@ -0,0 +1,187 @@ + +from argparse import ArgumentParser +from glob import glob +from os.path import join +from os import remove +from sys import stderr + +from dynamite import config +from dynamite.operators import sigmax, sigmay, sigmaz, index_sum, index_product, op_sum +from dynamite.states import State +from dynamite.computations import entanglement_entropy +from dynamite.tools import mpi_print + + +# TODO: what if it is killed while checkpointing? +# (seems unlikely but could happen...) +# TODO: add shell flag + + +def main(): + args = parse_args() + + # print this to stderr to separate it from the data output below + mpi_print('== Run parameters: ==', file=stderr) + for key, value in vars(args).items(): + mpi_print(f' {key}, {value}', file=stderr) + mpi_print(file=stderr) # an extra newline + + config.L = args.L + + if args.checkpoint_every != 0: + cycle_start, state = load_checkpoint(args.checkpoint_path) + else: + cycle_start = 0 + state = None + + if state is None: + state = State(state=domain_wall_state_str(args.initial_state_dwalls, args.L)) + + H = build_hamiltonian(args.alpha, 1, args.Jx, args.h_vec) + + # pi pulse operator + X = index_product(sigmax()) + + # the averaged "effective" Hamiltonian + # (we just literally take the average of H and H conjugated by the pi pulse X) + Deff = (H + X*H*X)/2 + + # we create Deff and the Sz operators before the iterations start so that we + # are not rebuilding the matrices for them every iteration + Sz_ops = [0.5*sigmaz(i) for i in range(args.L)] + + # a workspace vector to store the output of the evolution in + tmp = state.copy() + + # output the statistics at t=0 + if cycle_start == 0: + print_stats(state, 0, tmp, Deff, Sz_ops) + + for cycle in range(cycle_start+1, args.n_cycles+1): + H.evolve(state, result=tmp, t=args.T) + X.dot(tmp, result=state) # apply the pi pulse + print_stats(state, cycle*args.T, tmp, Deff, Sz_ops) + + if args.checkpoint_every != 0 and cycle % args.checkpoint_every == 0: + # remove previous checkpoint + if cycle > args.checkpoint_every: + prev_cycle = cycle-args.checkpoint_every + to_remove = glob(join(args.checkpoint_path, f'floquet_cycle_{prev_cycle}*')) + for fname in to_remove: + remove(fname) + state.save(join(args.checkpoint_path, f'floquet_cycle_{cycle}')) + + +def build_hamiltonian(alpha, Jz, Jx, h): + # sums over all ranges of interaction + # index_sum takes the interaction sigmaz(0)*sigmaz(r) and translates it along the spin chain + long_range_ZZ = op_sum( + 1/r**alpha * index_sum(0.25*sigmaz(0)*sigmaz(r)) + for r in range(1, config.L) + ) + + nearest_neighbor_XX = index_sum(0.25*sigmax(0)*sigmax(1)) + + magnetic_field = index_sum(op_sum(hi*0.5*s() for hi, s in zip(h, [sigmax, sigmay, sigmaz]))) + + return Jz*long_range_ZZ + Jx*nearest_neighbor_XX + magnetic_field + + +def print_stats(state, t, tmp, Deff, Sz_ops): + ''' + Print out statistics about the state in CSV format. Also prints the CSV headers + if t=0. + ''' + if t == 0: + mpi_print('t,Deff_energy,entropy,'+','.join(f'Sz{i}' for i in range(config.L))) + + # pass in tmp to avoid unnecessarily allocating a new vector here + # probably doesn't actually matter that much for performance, but might as well + Deff_energy = Deff.expectation(state, tmp_state=tmp) + + # half-chain entanglement entropy + entropy = entanglement_entropy(state, keep=range(config.L//2)) + + # Sz expectation values for each spin + Sz_vals = [] + for i in range(config.L): + Sz_vals.append(Sz_ops[i].expectation(state, tmp_state=tmp)) + + mpi_print(t, Deff_energy, entropy, *Sz_vals, sep=',') + + +def domain_wall_state_str(dwalls, L): + ''' + Create a string like 'UUUUDDDDUUUU' that specifies a state with 'dwalls' + domain walls. + ''' + if dwalls >= L: + raise ValueError('cannot have more domain walls than the number of spins - 1') + + c = 'U' + rtn = '' + for domain_idx in range(dwalls+1): + rtn += c*((L-len(rtn)) // (dwalls-domain_idx+1)) + c = 'D' if c == 'U' else 'U' # switch between 'D' and 'U' + return rtn + + +def load_checkpoint(path): + ''' + Load the checkpoint at path, if there is one there. Returns the next cycle number + and the state object, or 0 and None if no checkpoint file was found. + ''' + fnames = glob('floquet_cycle_*.vec', root_dir=path) + if not fnames: + return 0, None + if len(fnames) > 1: + raise RuntimeError("multiple checkpoint files found") + + fname = fnames[0] + + # extract the cycle number by trimming off the prefix and suffix + cycle = int(fname[len('floquet_cycle_'):-len('.vec')]) + + # path for from_file does not include extension + state = State.from_file(join(path, fname[:-len('.vec')])) + + return cycle+1, state + + +def parse_args(): + parser = ArgumentParser(description='Evolve under a Floquet Hamiltonian') + + parser.add_argument('-L', type=int, default=14, help='number of spins') + + parser.add_argument('--Jx', type=float, default=0.19, help='coefficient on the XX term') + parser.add_argument('--h-vec', type=lambda s: [float(x) for x in s.split(',')], + default=[0.21, 0.17, 0.13], help='magnetic field vector') + parser.add_argument('--alpha', type=float, default=1.25, + help='power law for long range ZZ interaction') + + parser.add_argument('-T', type=float, default=0.12, + help='Floquet period') + + parser.add_argument('--initial-state-dwalls', default=1, + help='Number of domain walls to include in initial product state') + + parser.add_argument('--n-cycles', type=int, default=int(1e4), + help='Total number of Floquet cycles') + + parser.add_argument('--checkpoint-path', default='./', + help='where to save the state vector for checkpointing/restarting. ' + '[default: ./]') + parser.add_argument('--checkpoint-every', default=0, type=int, + help='how frequently to save checkpoints, in number of cycles. ' + 'if this option is omitted, checkpoints will not be saved.') + + args = parser.parse_args() + + if len(args.h_vec) != 3: + raise ValueError('command-line value for -h must be exactly three comma-separated numbers') + + return args + + +if __name__ == '__main__': + main() diff --git a/examples/scripts/kagome/README.ipynb b/examples/scripts/kagome/README.ipynb new file mode 100644 index 0000000..8813087 --- /dev/null +++ b/examples/scripts/kagome/README.ipynb @@ -0,0 +1,197 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d514f937-9061-445a-a294-dd75838b0984", + "metadata": {}, + "source": [ + "# Kagome\n", + "\n", + "## In this example\n", + "\n", + " - Building Hamiltonians with arbitrary connectivity\n", + " - Eigensolving for ground states\n", + " - The `SpinConserve` subspace\n", + " - The `XParity` subspace\n", + " - Computing correlation functions" + ] + }, + { + "cell_type": "markdown", + "id": "b000d197-b478-4f7c-a514-7321b45be229", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "The Kagome lattice is physically interesting because its unique connectivity yields a frustrated magnet---with an antiferromagnetic interaction, there is no way to configure the spins so that neighbors are all anti-aligned. This has the potential to produce a *quantum spin liquid*. In this example we explore the behavior of spins in the Kagome lattice on a torus. Like in the MBL example, iterative algorithms such as those used in dynamite have proved a crucial tool for the analysis of these types of systems: reaching the largest system sizes possible is important for reducing finite-size effects, but tensor network methods cannot be used due to extensive entanglement even in the ground state. For this problem, iterative methods have been successfully used for system sizes up to 48 spins.[1,2](#ref1)\n", + "\n", + "In the file `lattice_library.py`, we provide the Python dict `kagome_clusters` which contains a set of finite-size Kagome lattice clusters specified by their basis vectors (see reference[1](#ref1)), as well as a function `basis_to_graph` to generate a list of vertices and edges from those basis vectors. We've also included some code in `plot_lattice.py` to plot these lattices, which also shows how the spins are numbered. Here is an example:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1d42842d-1195-4316-8dff-6ec2f54bfd72", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "from lattice_library import basis_to_graph, kagome_clusters\n", + "from plot_lattice import plot_lattice\n", + "\n", + "plot_lattice(\n", + " *basis_to_graph(kagome_clusters['42a'])\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7d24a2ee-c554-481e-b734-15aa12039069", + "metadata": {}, + "source": [ + "On this lattice, we will implement the antiferromagnetic Heisenberg interaction between each neighboring pair of spins:\n", + "$$H=\\sum_{\\langle i, j \\rangle} \\vec{S}_i \\cdot \\vec{S}_j$$\n", + "where $\\langle i, j \\rangle$ means the indices of spins that are nearest-neighbors on the lattice, and $\\vec{S} = (S^x, S^y, S^z)$." + ] + }, + { + "cell_type": "markdown", + "id": "25056b69-c283-4670-b079-96308ee2570e", + "metadata": {}, + "source": [ + "## Goals\n", + "\n", + "Our computational task in this example is quite simple: compute the ground state and energy gap of the system. TODO: flesh this out a bit. Also, do we also want to compute correlators etc? Maybe show that there is no magnetic order?" + ] + }, + { + "cell_type": "markdown", + "id": "ecad5b95-6da3-4cf6-a5c5-051dac20c26f", + "metadata": {}, + "source": [ + "## Remark: computing with very large spin systems\n", + "\n", + "Computing ground states is one of the fastest computations dynamite can perform. So, it's possible to push this example to very large system sizes. For computations like this, memory usage rather than computation time is generally the limiting factor. We can apply a few different strategies for this.\n", + "\n", + "First, as in the SYK example, it's helpful to use matrix-free methods to avoid storing the Hamiltonian. Unlike in SYK, however, this Hamiltonian doesn't actually have very many terms---only $2N$, where $N$ is the number of spins. So, shell matrices only get us so far. For these very large system sizes, the memory usage of the state vectors themselves becomes an issue. To deal with that, our only hope is to employ MPI parallelism to spread the vectors across multiple compute nodes in a supercomputer cluster.\n", + "\n", + "Another thing to note is that dynamite by default uses the bits of 32 bit integers to store the symbolic representation of operators. To work with spin systems of more than 31 spins, it's necessary to switch to 64 bit integers. This can be accomplished by enabling a flag in the `complex-opt.py` PETSc configuration script when compiling dynamite from source (see that script for details). We also provide docker images compiled to use 64-bit integers." + ] + }, + { + "cell_type": "markdown", + "id": "d6f83a44-b94a-4894-95f9-8a43e0cab54f", + "metadata": {}, + "source": [ + "## Usage\n", + "\n", + "The computation is implemented in `run_kagome.py`. It takes the name of one of the Kagome clusters defined in `lattice_library.kagome_clusters` as its first command line argument. Here is a list of those clusters for your reference:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "ce4add4b-3fb2-4ed8-a188-41eed8b68735", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "12, 15, 18a, 18b, 21, 24, 27a, 27b, 30, 33, 36a, 36b, 36c, 36d, 39a, 39b, 42a, 42b, 48\n" + ] + } + ], + "source": [ + "print(*kagome_clusters, sep=', ')" + ] + }, + { + "cell_type": "markdown", + "id": "df5eb036-daee-4f18-be4d-7d36d1a8d7df", + "metadata": {}, + "source": [ + "and here are the full command line options for `run_kagome.py`:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "dece3596-dcde-4e60-85e5-b8b31da6c793", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: run_kagome.py [-h] [--shell] [--no-z2] cluster\n", + "\n", + "Solve for the ground state energy of the Heisenberg model on the Kagome\n", + "lattice.\n", + "\n", + "positional arguments:\n", + " cluster which Kagome cluster to use (see lattice_library.py)\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " --shell whether to use shell matrices\n", + " --no-z2 do not apply XParity subspace\n" + ] + } + ], + "source": [ + "! python run_kagome.py -h" + ] + }, + { + "cell_type": "markdown", + "id": "56822160-591c-4d5a-b82e-d98bbc78152e", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "1 [Läuchli et al., \"Ground-state energy and spin gap of spin-1/2 Kagomé-Heisenberg antiferromagnetic clusters: Large-scale exact diagonalization results\"](https://doi.org/10.1103/PhysRevB.83.212401) \n", + "2 [Läuchli et al., \"S=1/2 kagome Heisenberg antiferromagnet revisited\"](https://doi.org/10.1103/PhysRevB.100.155142) " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/scripts/kagome/README.md b/examples/scripts/kagome/README.md new file mode 100644 index 0000000..f61c2af --- /dev/null +++ b/examples/scripts/kagome/README.md @@ -0,0 +1,87 @@ +# Kagome + +## In this example + + - Building Hamiltonians with arbitrary connectivity + - Eigensolving for ground states + - The `SpinConserve` subspace + - The `XParity` subspace + - Computing correlation functions + +## Overview + +The Kagome lattice is physically interesting because its unique connectivity yields a frustrated magnet---with an antiferromagnetic interaction, there is no way to configure the spins so that neighbors are all anti-aligned. This has the potential to produce a *quantum spin liquid*. In this example we explore the behavior of spins in the Kagome lattice on a torus. Like in the MBL example, iterative algorithms such as those used in dynamite have proved a crucial tool for the analysis of these types of systems: reaching the largest system sizes possible is important for reducing finite-size effects, but tensor network methods cannot be used due to extensive entanglement even in the ground state. For this problem, iterative methods have been successfully used for system sizes up to 48 spins.[1,2](#ref1) + +In the file `lattice_library.py`, we provide the Python dict `kagome_clusters` which contains a set of finite-size Kagome lattice clusters specified by their basis vectors (see reference[1](#ref1)), as well as a function `basis_to_graph` to generate a list of vertices and edges from those basis vectors. We've also included some code in `plot_lattice.py` to plot these lattices, which also shows how the spins are numbered. Here is an example: + + +```python +%matplotlib inline + +from lattice_library import basis_to_graph, kagome_clusters +from plot_lattice import plot_lattice + +plot_lattice( + *basis_to_graph(kagome_clusters['42a']) +) +``` + + + +![png](README_files/README_2_0.png) + + + +On this lattice, we will implement the antiferromagnetic Heisenberg interaction between each neighboring pair of spins: +$$H=\sum_{\langle i, j \rangle} \vec{S}_i \cdot \vec{S}_j$$ +where $\langle i, j \rangle$ means the indices of spins that are nearest-neighbors on the lattice, and $\vec{S} = (S^x, S^y, S^z)$. + +## Goals + +Our computational task in this example is quite simple: compute the ground state and energy gap of the system. TODO: flesh this out a bit. Also, do we also want to compute correlators etc? Maybe show that there is no magnetic order? + +## Remark: computing with very large spin systems + +Computing ground states is one of the fastest computations dynamite can perform. So, it's possible to push this example to very large system sizes. For computations like this, memory usage rather than computation time is generally the limiting factor. We can apply a few different strategies for this. + +First, as in the SYK example, it's helpful to use matrix-free methods to avoid storing the Hamiltonian. Unlike in SYK, however, this Hamiltonian doesn't actually have very many terms---only $2N$, where $N$ is the number of spins. So, shell matrices only get us so far. For these very large system sizes, the memory usage of the state vectors themselves becomes an issue. To deal with that, our only hope is to employ MPI parallelism to spread the vectors across multiple compute nodes in a supercomputer cluster. + +Another thing to note is that dynamite by default uses the bits of 32 bit integers to store the symbolic representation of operators. To work with spin systems of more than 31 spins, it's necessary to switch to 64 bit integers. This can be accomplished by enabling a flag in the `complex-opt.py` PETSc configuration script when compiling dynamite from source (see that script for details). We also provide docker images compiled to use 64-bit integers. + +## Usage + +The computation is implemented in `run_kagome.py`. It takes the name of one of the Kagome clusters defined in `lattice_library.kagome_clusters` as its first command line argument. Here is a list of those clusters for your reference: + + +```python +print(*kagome_clusters, sep=', ') +``` + + 12, 15, 18a, 18b, 21, 24, 27a, 27b, 30, 33, 36a, 36b, 36c, 36d, 39a, 39b, 42a, 42b, 48 + + +and here are the full command line options for `run_kagome.py`: + + +```python +! python run_kagome.py -h +``` + + usage: run_kagome.py [-h] [--shell] [--no-z2] cluster + + Solve for the ground state energy of the Heisenberg model on the Kagome + lattice. + + positional arguments: + cluster which Kagome cluster to use (see lattice_library.py) + + options: + -h, --help show this help message and exit + --shell whether to use shell matrices + --no-z2 do not apply XParity subspace + + +## References + +1 [Läuchli et al., "Ground-state energy and spin gap of spin-1/2 Kagomé-Heisenberg antiferromagnetic clusters: Large-scale exact diagonalization results"](https://doi.org/10.1103/PhysRevB.83.212401) +2 [Läuchli et al., "S=1/2 kagome Heisenberg antiferromagnet revisited"](https://doi.org/10.1103/PhysRevB.100.155142) diff --git a/examples/scripts/kagome/README_files/README_2_0.png b/examples/scripts/kagome/README_files/README_2_0.png new file mode 100644 index 0000000000000000000000000000000000000000..bebaa352bf28b329ae22fa39e52b5f8f3172c42c GIT binary patch literal 53207 zcmdqJhdbAO`#=6Rk|djqP)I6?>@5)qMY1xoWv}d6_6SKfAuF56CR_I2dvCJ$_k3N~ zeSCi3`}ltTf?r4XaUX~4y53&z=Qz*DI$wS-WhJiRP~xCasB6z8#pF>abPN;pT1(uZ_5}%`0<#8#^s4J(P@=jfIK1jmbOho3?sZ*6+;C*jcz)*qCn`+SpiF z^F4fM`ak~yi@BA-!%-Y;ANY_f7LqE~C=|XH@;|h6;k0)sRHxW8vBwJbv1^rf&T9Qp z7Z*{fp&Tof%9VC1Uzta(B6NAV-qu8-{B#N<8A9?}^$RCW3aVW~Jx#b&K8v_V5@2(g zJ~OHDNRj+nu9nw-J5wq1daL5Y-i=NC^~xyzMcLef&6B<8>xYI569)0T2eFH>J0xi0 zawPWu{@GCV{6yyN^Ut5A{H;b5KJuqi9nTzh>z_Yk^Z!5j;isW)^sSJjw0yV9T8HhHi)8O%NDOw7W<@`)g-LF#KlL>%nG<9TD z+X>Zn{K|?^;~Qrin?4$C_NB0k=1_W1iIbz9ML%O6DOZ-^c`M%VgoGD<4cdPuCnu*4 zEEtF1?}Y~opGQhbNhPVNt7oi_{$d}r(5;@Hp0<=dG(^9={pWt}6z#<}ZD>M5aBkJH z|Dh9nm#0sk=Gm^R-r%#rrlzJQr=`6!@yl$SPp#6C)ahWOVWB-{{T3f;s@Ci3;Zkxi z&wN5cLR0peUnrFD`F8Mj%bkR`_V(nDq5?}k(a$Y@R?kqF`Bi=P;7@C-oRX4H47WMu zDNP3*zmjID^U))DdHJm%b7zc;&5JW(wKAJ4ljr-tqE@>Ks!#v6+Al;ItkJmXaRnno#CqoKJO+ibGRl;5L> zoPq+a=0SCNIse%Bc>d3yo)4=I(KRoQ2YIid)v*1wZebw{*iNQB!-g}Fo|%d9_RxtZ zU`@?s9u7Nv>S2Gbt5;Xo%kL>E`8iKog?M-jua`R5+s9G~QRjb*jElR3vUhY`anUq3 zzCYoxNDV9I%ZkY%{4k<5`{8$*rIY2bi+VrelJ~1*c+`B~cLYC$Op*_CcJB$s%GkD7 z^-NCUhe~>$Y*y21X=xoExLw!{I4yT}wzUmu7{bX+^Wnz2;R&Z3b@tIxN$EPy)vKW~ zF#*>#okMp7cNf>=#JsB#gpMX1S08*&OQRPS)?9SDajTlGz&L5ui6?Ar#HFFL6aQW~ z&gAI=_jb0q%hl@B`G<1q>N3q{S&!@M>Z}dwXo-1zUuP5>Qe~t>LMaT3tdB5D=j3?Cg>^AClnQBEXKkz`;Zho5R0yrTe~{rY5ay zf>02$d5*bHT4{-h&^0wR!*p4%s~7w#F4i_O!hz?=&d$cZcCDeW?@Pz#G;@LVUU+S- z=;@%_+4b-8cXdQ0Q{+YWx3(|XqcBSMLnp#yYyLo^d_Ix9Jbk5$?qe<*k`ut zF7xZZCL3wbw=QF2qMuG(7On|)-KF>W(*ciHvDJi$N5{iMjxV@NfSTHxuB)HKrxCz5 z;@u5%ct?qj+8Qy=MMG^=U$}lxO|9+ieYJ_16P%KAd#vpJ)w8oRLsL_6b#-dhH*dNp zCxhSoGShnZ?n<<6@~xu;rwv}_2M?AxFgfNYf0-w!-(gTMFkU^G`u&lw+K0U*Co@;z zV9c7FimGmDiAe{WM;<9-i(Zoy7B{ay)TmFDiMa{K`e-phr|A>*Q@7%*EZlOtO>e0b zQK-LMW@FfI9Yo)#snw5--Plo8R?hRU(9Vx6a(emZO#obD)z_|0T35$Mp+qA$&$@OK zmnY=I2lTbcYFeojB7C94n}7cN2|QL)QK8u17{3}W<#l#)ko@hN`%#R@P4}1>Ds*&o z0#Z`1zCHzA0|R`s z+%t0C>sQiuC~Td#8rb*^U#e{xM_pb0a68y7aAU(PZ2ln&OQS)8%R-)2+1Dl4(PC4k z2M^vyu31}KH@CDrc5*uL@BOF7{HDT?MJcHtpPo7^svqB1%E?>lG0@l7?u-{aJU^&5 z54YCzPs+>7t0BiKEG$%3RqZ*U@DlYXH0acJb)`vgJ-$mxNg4Rn4f=)sPCGZX&@Ct{ z(^Qj(SVG53^r`LS;}>ohlNUZNC;Pmjx~Li`-IgnT8KjwZ$o_rc#)|dy^Sg4oZthmM zJ=Bcr*!U0hd}0TUffTSsTR-Q2Zzu1iGc<-PDosJ_sYbr{kW z?k#M#x3@2CY+&V?Nbm0LB^DQx4d!Yte$iwmOHNP6Mvm%0t|o$28ZSidg^$eflYdBJ z;^5*+Dk$jD9T(oNCzke27mSLIuFcG?!by>DS*k23AcA`vTkxELXl?Wt{tLl95*!?y z8%YMpLwh3+-O(W{EhB@b%!LazKJg{fx0g&)%gcUnlT9Y8sA*kx3G?GFn(2dY-o4wh z@JOD-3vNMMd%KpQA-3De=;SxWOqI=+mKG0%d*Tibyp2swY{J6$P-!r+u#!qj$j{Hu zV+YCn#F!VwqNK6%hxXsGE-Wl`jg0&`b<);GU+hfqkxHp;Yr{=;%24p1p4J*LsH>}k zukvEZ#p>*EHn@9esM+W$Hnxbfvw&j&FBIfGY!34 z>_qTjtnSG5`t|F&y1GNE%&i48W=ky=OrPb9|l`qSE z8Lwd9;Oh>tr&Y-7DZ_~d?lF#6I zGHhjK#iNG@>t<`qCI9LbA>3(qcT{a88<~F+D>t`Kemu;yT__ahow%9YI%{nrR3~b)#U1PD}skUcELOptW_4bblY8#f}fT zGE;9!a4yN&JY^i-znxOy_3QlNUjS>6)*|S%KP0V)O8@brwmpVh#Z*3n1lD&k zP{_j4Qp<|A>j>XnFIjYyH?;Mrod9++EG#TCHdc3JY-~)q>1s|+PHfZFRC29dySw-9 z)%p?NQC^RDsM!dh(o{9!EtKB07YU3bS8;Hr*4Klos)S<3W01!}I&d4nCOLcMn;}2&O}1g;YSxLnYOVP8XD?hJeZ?pVG;7~RfYo3 z`;{*;Pp;j+cMq1i$!O>AU(v^p(USS$#?e?$2cccoSy}nQ$i+nlw}$zJJqqd>(h|*x z@ehzfmKLF^}n9wn{cw-^u*;P<%^+FOb1Sy{jCcM581@%QgJyEWwHYny90??%NO zlOLY=3o$Wav$3&BzIY*_s7N+5J4;AOiSzK`!!FwdvyDM zyl(#Uhi(M;f#1vL#uk6)p4D3T`TB}DI`W~+DxqU+MWR=;Pl=QiXPlj$25R)5935!^ zfhe(ACdxd6(=RC{RiOHncO*dF$to~7SVB}3eaC$L*)t}RXhD@#01kDdqf`?U6M-f_ z|e6kwQHpxp@OXQBr9s1&-H;qf=vjN>Lfb(8G+b-$ik z`Ha4~JbtK;fVh&*{?4~%J!@0@B6V9fUf|i!#rgSLtgInNzob&=iVLX2FEp4)YU}DS ze*gaM8xZhBU!U>Oqelc(Q~^^{Zv$-a!zaS#dB80JHVl<7Ztnm&{lab&J-Mj2yjCA4 zQodPQTHbOsx5lML&a4bDtj9|uq^t(F)=23fQl>n)Nr#3f(p&6$JB{3oJLi)?4nhqD(Tdh3S z-csvD-{xyj5C%Bk+3SD|&?FCE5RI9cnc$X{nVA_`9J_>=*iCx+k4$f0sjB+lNpSAT z`ci{b7{yaqQ$QWJCd%al(OjNJN-}(ih`3%+QGp_lI1#x?aPz@~Yu8!S-l|(WJKso7 zPL>YBSRcC#5MX0tBkNDlPwSbXA@UzTeqi%Hmy+UnLhJDV{eER@C6TbCB)Xr)#RZI8 zKqdS7`z>Xsj>&J_5OZ-6{O1n7Q_jNF5lMXc*3mJ#|0xwpjr@;`2>%^N2M1Ul9B#9* zw=Pw1&OJOlVvmzD>5LYHpln#!*)_pQ^BWHs8d7FpWF#7t%k0&=o3M4sN>R^C>&Ha6s@ zCIFB)xVeef<>cfd!&tZxze;-rrEO+O=|G{Hm!=&F7wmViA@I;@8x-iLOjXnO|{maFD*mNC}Ye z_wUF51%0PEzBoEM`g%Si^7DKzAhIY#PDv>$A#u5^tSpOB7?D8&b^-_N&`89iUf*a^ zShD&Z4T406Gunt+g?%U!53IC@ObplZDXvoD4Fi6Ko zFjq(jL0l<5A0MA@r)u;w`E11<2W+^cxV2Omwg5l8E%yKJ?*2Ts<)b!AmvM69=i}#p z?`mdcMFh=|aU{Use^*xWUbsI`t1Xv~h|-FIF)=M|=kiihct=!+3nKcAl~{f(w2;@; z{Vx6&ZZQW3$2aKzfPToSsT*vRPK z_IYCBEjZ4E>rVS?j{ydUp47xLJcTlCRT`^DQd(YKX!H<{at(0WMdZ7RW-AhBzVZ-3 z0g8$lw?5flH#9QRp64iW*kJ_r($(MZ1G!b*HRM0h6vT_W4p%S)r zcCz0M7N;X@<>_j3$?Jh8*pRQ&r0n7ft<3&#+9yhlK%m%T@a&I>NEH9)t;qVhIfm)A zc|abAhxRcO2YeVC;7Ybh<>fI$6uy3b`NM2zL4d(qA65q@O9Uq+CVITngxia74ZuHb z0R5ojlsN9)+c|*#1f`#NVp|Ot7x%4=&6obKv9Y)S9x5MdGeXOQ9oH%@K-yGwFVLP)M(pDpYZVn1}ZqTq{)ijoaA zaBqEi#7$SIy0$bPf5ysTApbbS2Cptcye+6^8H z)MnKQ3qVYv^FtlDEOk?haH8|4Vg>cW4^ zJqH>hjf4AC6Xbcw?Tn0$)-GyjXc+EuAW-h0$OD|?+o0`yezf>hFp>lG)1gWyZiRcK zppoWT&OVLXG&MAQ8+TXnit{r_-CcFO~G&_19yZvC>0HJO@h@7SI@^vttDk;y#d)F#2wVp zsWjoUyKs0{`v(V`)auB`tsM;CI61v8WPK$qjoZ`P8yX(&1%&`wS8Z!+tH}k5Z}WFe z4GI?RB?zcrzmg#g5{?Ea5Rcv|eaP%Q*xkL!!h#2s2s$!y-@nOcZ1uf*@f&nJKR-X9 z2YNYLffEjrfy+Bjo;+#p=@wInWYeQ^IBsg=6i`IOb z&PVeRJg%oa@owi1h!R|V)NXEZcKm+8320$RLIOUtx$5)1+}MNZ=#bdo{NiUhJD+c*EWG^z@wSN_=wo8{gF(l*QeH1D(}@Y~P?DaWOGW>uWkTo9X^u+WF)F zy|Ll`04y&kCdKy!ZBBs_DtJ*!;l$cYHgC~$r_3shsV0xQ?4$;v>N z#8MuZX6PcQB6YpJy$c7aty5-0c^*5P06aq^8GQZy(Z%JYn=IAJ6 zpK7K@Hyt-l_0}lVzxE?D^Hx|3kR~qxg$fD^QwG8(6!|~6UmtwJ*(S!uw0QsC3-W_A1g6La(az^96aZw0&Q;o?Th_^HtU6QV-{z>Z|LR_pLvIK0Bl!h#UZ zjM}>-IMW*&pWqImzE4cLUAUaq37=xjS@G)Fw70c=tFES%k&!{w{8)OI-nkPJ*|WI! zdv(GR;M_IKK0-7UV^MaF&&pDFGScxToOfhE8k{a&jIT=$7VgFe%?G4jm3L0sO}eCKXE%Uo>8u9DJh(!OOKxH&X(RE>%k0cy5p2{h&m1swk*-#GzP6pk0JCGzi&4wYp*=+XZ6FRwI@9w;wS~38nxKVb2vI z(8n%$h`1x3gn88wFk0p|2F5Xk!6!GKBhW zDtr|aT~|+U_`&`7;teWFN?#rK_;{MrH3Q+g2u)W%KwR2`wILg&L@H1KklwGy#Phu4 zvNhZ7gWvV_n6{e}*FbXIJqi{&#_kl@!DVD*Bp@U61_9oO37UVg=_o5NuU|!lK*jm- zs`SPconrxfzkC+}G~WHTzy?jm%SchVP7-wz#6#E;G=!*D@1`gCf|ygA+ttxs6tzIY z**H19;DpCWqCaUm*%v3-7ROY1+Py9p-~~avg!kjR+_Hjtj|rC zsDGX8kDAZUwS+OcAOb$P9+yx8dws8BH{~)E>W3?m?=VQv5_S!#9@qu2)p+S|d;|;# zLgGz!c48=FhaGlR7&TUvn0AJ#o~G7FB0~@ zsQWkK?yIH9BXEL=iD}9?p-@IrZJ5SO^wunc{tM85x4`)ukv`~%p8PEb5b^NvaO(VI z^H)hH@(TfH5Hxtvk&s7@eVXu zFlUfv41I<3IdtX*$M?CWzE`|NeH3wQ$e_`lpHyE^!J6nVyR=7h9tK|=;#Yd}$DOL@MMEpnekK9K*1DEZ8Jc9u63}&Fu*Y~~<&bEG336uk|l#BvCebOiD9E#cBx24Ns z7807c#g757!v1V;5JdB~o-?P-%Cy1l+^VYhdA*0>QlRX70yUZck4M*y^W(wlnd7}x z{pGV74~(G-hr$M8B9NUy=uY*Dibby0=~1Vfw|oZ2^8*l-;P`!}7h6O#$5oDle?}+v zP(a|uty{NjBT}Fsn^*3UxzAGvfP$l-sF;+U9c2CbA(3B5bhQ82n3~wrr=oxvVbNn9 z>d}xzg5Q!=TwgEdQ;&V01ZQ9hgW13dK>g1XsgFra6+0dLOY7@;=i`HeJ_7@apxRQT zIKkcc=d^Zsfs$2JRFuU9L;xDw#9pI_ogMdt%Yiy-O=uECe2S#>%uWe$akgRNh_7F> z=%}bwrK`cNr7kemy9$Uo;o{VQ@q_=s8Sr(vH*bu;YyeQ$0+ZzAQ+JEhM*xT7vCF=Y zB2YvlgMCX+XIg_J7{g^+3!ICUhsQT6iUM3(I#>YE9AL9`)EB{C2S8w;{gcy@F#*kB zyBGDfFsIuam6vq_cKazTwzq0FqYW)Nxi5&!g^HiR|E>ifz}xvE?%8ICr)u`X+79U2 z^*>%`fMqhyYb7#c$gt4r!g2wq&G)1N7D7 zX1J8n9GAnBCm8p?L~rbXMbCps!sGYDrLt6uaKP|D)cj1Gg@2_+8x|pZldG_mp~Dbd z=wE~m6XH>pn1}{gE{iK)IS@o2UTU#?4jAJQ91ayzr1OCe8#W#Ueoje7PEOKcEy1Cu%T zD`yY}5W8P(Q44ett%#qMBnY{UXZOTcK>+r>+x&ieMtsH^G^59=s?p>0x%T##MKfp8 zKbMXmzXgO_u@jJarebyt}6>ScLW=mApnt%Sl%ZU5!_gku*epOXecNo6hG8YgpCgcat$1&ON;&PHSoD#`r&whjM6Z++gCz#{;RDNt4-fkRZB(mvr4|$v zL|oXhvDjYTI~t(WZ1smqrGR8gKarUc@qTrHaJC!d{*DA;b7EeH-8w1^MWAqLY*;KB z6_N^E7YxrReVN&f+RkmGQi?eDBLFwheXrkW=}4eSIw0nVTcO%qPt(Qs_x= zcsw17SlUQ#+>o9V!oG6F zBP}g$*!wRqOtbMaFF+$84<>;x2;~fk^MJQ;E25QJ|K?bgOAPzG0|21KPu}{%{Y8iA zSy>FW_VygXxL72Rl~^)n|u>CGozi%hzl3)^JgM$y7jd+G0?Mi%v0o_ z;fi$V;_}pD^Sc~#SV|)g^8LerxSHCHH17A1E_ps!!B(RW!bNy^xR?`&I|d~w#W_%> zGv`=jtgYEw59$<_#xBDlmz0(k`!DNBR``a_H)~o*BZ2fl6%-U?XksD;!3YSfu(Gr3 z{2IFq=exdgX|v6%cmoWeD<6Y2oOwp^n&TjtrF_L8Dg=?c1!5sl0N0=Um=Y>tEU_}k zQr4#M4-uuDG!YI2sc`<~j~_p#=jPrS0hv`=m2hJy|D%1ieujF@(O-TZBL5_wJD7x6{JoWN#Hwl>)Ax4;4?vwM8J-3S;9ql_enmzP()>;#k@!p3(pVswO% zzXEHh^IwNwPY0Fvv0u7-<;r)_1`lVmkG5=zDK zvlH_=x`%u3&=owq#%NAsU)y%%E0pp}5Ztc=m6PcHoPnK?DE}Tw%^HhaLT-zCE}&Eb3+T%bAvGcM7|`NFc5FV#ZK z!WZqtOpp%I#J1O-F%Us(Tseysa)As}e6#X1pFuvH_F8FJ?f=IkD|{u_>wu4ih1E4M z;0@~C&!0cP<>tOH&`S04y41lG;pvH4=}K@Ms0gh_@zN8d7ZHStg5JoD#ViJp&dA8W zpOFM--BIk@{Vb#2R2eco3=%)FSegw^PEH6+-5OaJyvzIK!=0SGdtl(2jQng#S_MCs zo`d5{f2zVg-fO|--Q*B{nhzWUV@-ScqcM6<8btiG*gJKTwGju zJL6?;3klI&xpGD91nR!>B##5T`%B{A*?8rGEYJqZb}TqVX;kf zc6KJ9plC5-XN(q!fs-#LE}lQqXH?Xp=dkWq^-pDiGz!}Z&ofY5Oidpc%smBZ==oqO zrI!17gOBZqHO(rNYOPnwpwa@YBWU@-usBA|}z}I{Gy% zyk2|x_*grp#5@6_Wk4bcP_mVvd^FGlBx^cTuDlyk>BBEBp`&S~gB0=6o6pqu-NE9g zPoGHVr)3wB3jG*LBk16WAp#H{G6?{;>gL1)Y$q{L0Hc!jaRx0t*v>ehY3)Qx@K=0o zItKQ93DkG#7cUA&;G}|mv}Wa|uJszui(>Zq{J+w5Qe{etR}}7MG5v>$35|c8j|DNa zB?yA}vaSH%>6)DEJUuZ3{x5D;-`Gf0T@6muG@E%=4rumBt|rMP#i^vcq@)I9BuF%f z0;NF&w9lVwNt$}>7KHiu{zT#W-nV*kvV_Yz(H(xjqBu7Ec|f{zDkg;mb}`v*cCb;b@^NJn$055@+8GL(*7*|zoy4o<=G z891-J^u%C^0Ka`Zc;y}VCC&SwzrOoLP(CuEN|z-?7i#eorTh_?jCM_U>wMeSy>TMd z^p{LLuP_%E7m36CUoy=F-eXHiSi#8^1V@;9eOh+Y3|us2HMO~{3#}DAhPab&zkXc@ zNd&r!1hhd+zbsi|@ zYRJT1`=M|T`uIpcxy2kI)Hu2{yB&yqnb-<2gi7*QBW(eksodtzzZNXOb)Hd%#2N+# z1x2ib(9dg6Yj)MYz=f)voJ{C1{`s?>qS89;5fD`XbcE}GYJc(sDk*8Dg02O&DdA?d z{Wcw8WrGei@I`6x*w@34WR;ZrfsLC)dn+-KSUHy9v7Fd>;}HhG-zmO zpiVNL-hn`1{##bkGS)C@MtUouF27WJ15xB!>d$}*m*d0E%1QvT1Xr~kG+DWR0%ch| z?cmEcU~7`QBiDgwz#ecSBXuDo5JIl%YLS~Fprv&Sad3zo295b4R-W>*4^*lL4`kAa zbBl;{ZZk04ge%sc+Ujs1tD~!n3FT1C>DjX@i#UQ{j`xy-1j4vU2V@iSd_?O|+j*S! zneQL`ma<*3A%nHS{rdH*Z+~Jyz*Wmey{mu}d971oR0*1hK-Bbg3JMIgAX~}j`XoSy zC|&tLJgI}AI+c$l$jW85OC4Bn*F)Fhu@kY~7(*g*aSlRRQMbcZk9uCGFUmfKd!nsP zH#0M1!{S@Ay8&JiL8M|X{m20#gmA<-}E z1?<3XpDpE;d?Hxy% z<3@TUf5Jw-1jyt@$+WUgKq3gQUzez7=389&A^+JC7y_t>-Zpasb7f`4x6lF7p3pKt;HHaL@AZqw{1$fGdlaYwc0w#P6fpW4 z){Ob?AJcKeddhIs+N?LjUeg^QL?CMHGMEU^HKx^^4ZR1gkmL+_IN`O@b{PAuw8jQc zB_u3Cq(v+(TU#=SJSZ0#kUP?O`1v$CVWlSQF# zJG3Yf=vjEQLTy%L>Z+=uknlm&6D}^0AHRLO2@V;t+|8dRHvjTnB!~+^_Vt2MgG}B( zYM6c0HhH3QB*1}u>wNHMLFb+MNeLD@jpNcSfwSFS5S&<#BxrBkU{JgVOD85r@uryt z)QQiZKY;WH>ICB1f;1wkok&F{pMXlNnzH}#u8pnjqb3HBV1ViTES)g*<8?fM!gWw}APABc)K{YX z&1cV^fuHTJ#v%cZ!>V~zRTaogMUwiU)C-?2zW~=;4^hv<*}rVd_MEvKjQhmK-hdjH zSXdagrvZ^B{iYvjX+ErDU^G0SNu#8q+TL!WrRra7&}M@?cfR?g5b(CB?{=Y*^^fp$ zA4T=w@LXq;h-#(Q!9_?g*eCX2a6}dZo74gfxwX4n4^c-Xt^Fgo((S^v;%Fh}>6>gD zrikMCn6F<`xr5Rk=t9`8p31!9x5KA*&wpP20LL4$X?XvWP@Pm5hnQLzPc=9)i-9Jr zm#l3-U{-~Hrz0ojv&Z~y=dK(IN;c%DCLmlMvofw2Il4IMdSVKx2Ay~B{4DO-fTIcd zE1+!v4Z8;h8jqbUEb?AI65HS3pXqcvFKj=l$Yzy*V9*f{Fjb_r-Z=qVNB=gMJ+f=s zIU1EC_nw|4obTSbA>=&7@Q+Xlg9sN|a#k!E3V$BqfdnKI{~gvs1@ryoypj@0((Bh> zp_uZOr3E3|sQ_ng(J3=CGXoi^Ao3FF{~kMg@50|-MMZ>V3DMC7OXXP{%A+5pbnm!n>8!cwiqa%WdVNy(u5K;vDR zQig)Y58oL z7ybqZ1rav918(3qvX`3IV^a;K43b)?{oa4R$Rt=naN5}2!lY8NRvaMZ;Q3KYO3J^1 zf=@}cy1QPn3_d_F_A)8XCF85K?j43GiRO+JJMi?8#9Tn@McoZ3g=CR8rn9^F!XIm8bDJ9*so)~t1yk!D_*{QK0n*?5!TJo3p@ogD0bBHt4WmQ^7;QVUBoFQBuh$4DhDw$1q%z-z4FPk z4u;|EE9;rRQ|xlix5unI znHL(hcHS`GXsq1KtnSk`DOv(|5*xXbqY zes0AFx6>v!F;`cis^gXWsMcGC)o1JGavB<+08ym6cR*VtA|hfH68Z!ZdxYM2&YNF& z$e_^K!DKMRs*vc&*PCY-YEgU8^V*8ZAF7qOg9qh%yPV~ysj(5vv7nL?9z@SB0W1vn z{N0L*H&;_X0G$#j}gLkWoH=rlqmvUcqAPs0B zyNxybInFo>8QDVIAMkpR+;?upUmWn1qUdprSL0OoOFyb+>z`*{8{GNfSCy|$thx`j z9|h8?8w#xRlfv-2mzlUthH8GojN0jvgs|xI=T`xnMeZwUx{yM8F4o};#ub7ym2$P6 zonzVagISnc4xcXG>-s`H^;H-$!uEsK|Cfv3YVn>7 z4!G=~BkT7A$aOvH{AOE)#uDfdd>2N>K;`p?(}0XNu(PuxQSk1*K2H$GMO`IU<1;(m z;8HryFZNW@>n>6nBQeO5l9DOdN^w@pvs{xh3pJrN^B#~R+cX$}`>2iy1;TlnB891eH7 zHvZ%nwQWD4_$&;f3+VB66O~SYPq)_!I}3=ad18><*LDa0CX&QJB9%bl^C4UhE_lV^ zOaSwf#wgvve&Gpd?x-4MuFA2Ry)`}|VbjKOup%M#bfiGD%*L-}MB>5@_T)XaL62a~ z#tuY)9Ov!0hB+zV9CjQieO@bzSC?7pj44p&3@2R|k-V7lCfNmZA^H1vOu$em5tHSv zgkJ`a2#z%x&r4v@XhF%gTSJ%S5PPZx!oHr+0q z6e->s^tk-j036}P+2lnK$negPOFP`Ks}68G`Q@ggve3a$o^x(F*G#xJRvMOmy3kf2 z43JVb&943A-JjeCZ!6el0i7-f??z6fM~_s^zufiW=&i6^v<^!^pn+vPb1 zO2o%IjVv_nTH>3yUBmSV;4(5dHHG0d5|9lbTqOtQBvNLi7t)|A!f@Cjnug=@ZPZ(w zAWTe5!gnbocV5nbc_{`CX@xCSZ0lz6ld1>k6 zd*R>>fVBN+bMOw*-T#EB&ZIqla{YSGmrC&LfH80F?KK$ZR(iwS1yHO7<+_H3OAv}j z#tQ)wU;ATby2Nh5QmI*Pdkw0+*hPAJx(H-EU7_<64;H4aai}>iejZn@yo`xi)7ly^ zq*&BY${BI0R$!p3I|WK2SKpc-GWL9NbK(7wDV%=i{`5XwE|<| z!hx;+sn66mo1%eEoz7gZt^u#q9V%@}dASbgi2zo*%ApWw9FJ}62JN_S7${Mx+8$Hn zUE#8p`H4{`@HcT@+y%DiM?t~oA6Z%7A?J;ZroIBvk3?J!L2hYw1=*U%71BlX7clAc z#LL@T9wvSjKug?P$;>4xP*PBM3H(_BFa;+Mk0dw&av=Bj!R*&<7`^fp8v2s*x1I`4|5SdL0{fxPvxcM3d#0G~XB-TJ()URNHpU>vvJJA&MC++X9BD!`K^ zKKk0s$Nd8X`ozO`{heYj8XmenzJ!G(557>J53Q?Yv)QMKej`p!Vc{2`BfMN+Ul)_V z{XFT4jbM57P-bV=Qk(}w^O2n9;nYWbi^DA~#`v%voeY@E$v>BAGTRt0zYbMZ|FbM~ z>LFPR=EZ&x@5R9Qcy8uXLsd=qDcmA_ltDnb1Cg3;0h+spM`GjKrM%mop!hD2gBg9AB9IhGkROSffK6jNF z@9ijxUb>%alXbS}((G%d$hAgG%B zm!e(j4+A&kOF*q$m~_UfofoP$=%mr7QVjd_32&h-vXRouD(mir)WF%Od36vZN{Iu! z!CU_M>sR3jD5hBjMW)7;nf*qPDt?6JUK;Qi!l9JdTK7Is4OlyCbbI)x% z1Eh8M!V%&H5Vi;)E!Mr;chT>W5Es_~w+L9cH^k3(_xB5M(NHirQyVYnl$CP=rLC6W z+iKvOt8ezG-h8XYqrY~7KDsS7mdOmg&^KP%iL%Fhp= z_QdKWWZUYZR#)SanBWXSc!vZoAQGJb4HMK487@pjdAM;S5c~%u`wq3FdDnJ1QXm!5>`o6Nd zs$%*tdvB`|wM&QZ0Sl}R69eqLp|^%FrfJX7zyM%ut%95>P)TyI-(XCKec3Bpj)??G zy;<(9ybhb6EHcE(l>HMPJ_p_4Y2_!cmo znA4u4Alv+#K3G6oF`=`wvk5Nm4MizC7#4$PhMYCm^Zf`7$170feS?GVOuUdI0WhQ7 z`2ptj0PbcpR^OhL0nt(D;?&w_xeDOY!K7QYic9=}3z+Q&M;oxAGvr*LIfmy95ixh4 zfQW-&R+x^|-!tDBs1~EsQaq23kAF(-1^PAJojaFdo>Z29V2rR4l_yXma5NW&z>lb6 zBz%&+1mP2cz3zkcQ6IPSMK|zRbkxmZhyDl-{-6Cr>g&%nEP?*^urUIR|6;db^OB79L@r=UbQ&`Fm z$g}BJM6Am|`r#9;uvx(b=W0(=eZ9qThy}$qhy$_SA8$wUPgKD0dEF@6FFr4^IMrNMlMq7hgB|>i{w6qdcqMnx)pUY7Dz>Lfmgm8HF zl&zb{F>3yL+Jg{|oDW*p!&_Klm~*>KP` zb&o4xnhZj|;H?;>Po16$5T=>F^Lnlxnd}7US74`&y?&;S26^E^nrs~U)`1X`wOUxv zk8{y@a{#4AIz53)t%?%dSu!6RBOSAZ7m|{1t=h?J+uMVpP6Y)7nECW!cDx25UqCsN zPb%meqxt`kcj-c90KcqmYU(D$6aO;@3_gNo{IK&P|D+aQ>3_7eO(H)`up|_ZwBXbr z<6IC1`ARA|dFTH9t03IgK?DVUibP8x(Fl@a-Q=X^W>E^fD1aO$S|Tf>Q<-q2Qa-a0 zFy6Tn)ziQsni#TU2;(K_kOA;Kg=SxWQde8c^Xm_@hwXWANIY25-k z$aB&T;-luP;+AM5SU)OACSU0((x6wEBkifgK@%zCT|3$fi0(M&_LwwsOSe_Q?alMh839nXR^XVz|jT=4V>Jl zc??K3)-$|At6?yQq2afIF#V~$Rc=MIdRY zvkpYZ0Kq6;rNTVlz|6S6z6232zs=d{>E8_b$Bo=ozaKGV%{%Mkk%$xCr&WIgee1XV zT7HxAQnvZg`wU#=_+{z1<&SY$&SS$&{++82bD~g z>r?U7XJrfw?o2=iS@8|bL|Iu|LwLc+y3`N`h-H=-|Fw(=aJ)R%VEtigQ>Tc+Y6iag zgWVhu^kG;c_5|vDGU$l*Fin7r4|~0Vh7JxpYq=4K`fYRO?evl|Fl!~JqVnwLCYT&> zRk-f+>EhXCv3&~$O$p`*@)<{AXJ7#25{&J01zD!?S})*kZ^3IOV7erPjSONYDjj|! z6Q_+Zl5H*Um_AfeS>h_JCf2XNCB-vKChmZBv1pw~5|EHTBzX1Cvf;yk+7IC zTknsCg7mvz96n{J%-NJwt-@(wRN6%v;NI72@d z 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 HPG|oYr#@?@ literal 0 HcmV?d00001 diff --git a/examples/scripts/kagome/lattice_library.py b/examples/scripts/kagome/lattice_library.py new file mode 100644 index 0000000..3339c1a --- /dev/null +++ b/examples/scripts/kagome/lattice_library.py @@ -0,0 +1,170 @@ + +import numpy as np + + +# The Kagome clusters listed below are from Table 1 of Lauchli et al. 2011 +# [https://doi.org/10.1103/PhysRevB.83.212401], except 48, which is from +# Lauchli et al. 2019 [https://doi.org/10.1103/PhysRevB.100.155142] + +kagome_clusters = { + '12': [( 2, 0), ( 0, 2)], + '15': [( 2, -1), (-1, 3)], + '18a': [( 2, -1), ( 0, 3)], + '18b': [( 2, -2), (-2, -1)], + '21': [( 2, 1), (-1, 3)], + '24': [( 1, 2), (-3, 2)], + '27a': [( 2, 1), (-3, 3)], + '27b': [( 3, 0), ( 0, 3)], + '30': [( 2, 1), (-2, 4)], + '33': [( 1, 2), ( 4, -3)], + '36a': [(-2, 3), ( 4, 0)], + '36b': [( 3, 0), (-3, 4)], + '36c': [( 3, 0), (-1, 4)], + '36d': [( 4, -2), (-2, 4)], + '39a': [(-1, 3), ( 5, -2)], + '39b': [( 1, 3), (-3, 4)], + '42a': [(-1, 3), ( 5, -1)], + '42b': [(-2, 4), ( 4, -1)], + '48': [( 4, 0), ( 0, 4)] +} + + +def basis_to_graph(basis, start_vertex=None): + ''' + Given an pair of basis vectors defining a tiling of the Kagome lattice on a torus, + number the vertices in one such tile and return a list of real-space coordinates + for each vertex, as well as a list of (nearest-neighbor) edges between these vertices. + + start_vertex is the coordinates of the vertex from which to start the + breadth-first search for numbering the vertices. + ''' + + if start_vertex is None: + start_vertex = (0, 0) + + if not _is_lattice_point(start_vertex): + raise ValueError('start point does not correspond to a vertex') + + neighbor_deltas = [ + (0, +1), + (+1, 0), + (+1, -1), + (0, -1), + (-1, 0), + (-1, +1) + ] + + vertices = [start_vertex] + edges = set() + pointer = 0 + + while pointer < len(vertices): + cur = vertices[pointer] + + for delta in neighbor_deltas: + neighbor = tuple(v+d for v, d in zip(cur, delta)) + + # wrap around torus if necessary + neighbor = _translate_point_into_tile(neighbor, basis) + + if _is_lattice_point(neighbor): + if neighbor not in vertices: + neighbor_idx = len(vertices) + vertices.append(neighbor) + else: + neighbor_idx = vertices.index(neighbor) + + # only add each edge once + if pointer < neighbor_idx: + edges.add((pointer, vertices.index(neighbor))) + + pointer += 1 + + # finally transform the vertices to real space coordinates + vertices = [(x + y/2, np.sqrt(3)*y/2) for x, y in vertices] + + return vertices, edges + + +def _is_lattice_point(point): + return point[0] % 2 == 0 or point[1] % 2 == 1 + + +def _translate_point_into_tile(point, basis_vecs): + # Lauchli et al use unit cells of length 2, here it will be more convenient to just use units + a, b = [np.array([2*v[0], 2*v[1]]) for v in basis_vecs] + + # "a" should always be the "more clockwise" one + orientation = _loop_direction(a, (0, 0), b) + if orientation == -1: + a, b = b, a + elif orientation == 0: + raise ValueError('basis vectors are linearly dependent') + + origin = np.array([0, 0]) + far_corner = a + b + + rtn = np.array(point) + + if _loop_direction(a, origin, point) == -1: + # point is below the bottom line + rtn += b + elif _loop_direction(b, far_corner, point) != +1: + # point is on or above the upper line + rtn -= b + + if _loop_direction(origin, b, point) == -1: + # point is to the left of the left boundary + rtn += a + elif _loop_direction(far_corner, a, point) != +1: + # point is on or to the right of the right boundary + rtn -= a + + return tuple(rtn) + + +def _loop_direction(a, b, c): + ''' + return whether the points form a clockwise loop (+1), a counter-clockwise loop (-1), + or a line (0). + ''' + v1 = (a[0]-b[0], a[1]-b[1]) + v2 = (c[0]-b[0], c[1]-b[1]) + cross_product = v1[0]*v2[1] - v1[1]*v2[0] + + # return sign of cross product + if cross_product != 0: + return int(cross_product//abs(cross_product)) + else: + return 0 + + +def _test(): + ''' + basic checks---just that each graph contains the correct number of spins, and that each + vertex has degree exactly 4. + ''' + from collections import defaultdict + + for cluster_name, basis in kagome_clusters.items(): + L = int(cluster_name[:2]) + + vertices, edges = basis_to_graph(basis) + + if len(vertices) != L: + raise ValueError(f'{len(vertices)} vertices for cluster "{cluster_name}"') + + assert max(max(a, b) for a, b in edges) == L-1 + + d = defaultdict(lambda: 0) + for e in edges: + for v in e: + d[v] += 1 + + for v in range(L): + if d[v] != 4: + raise ValueError(f'vertex {v} in cluster "{cluster_name}" has degree {d[v]}') + + +if __name__ == '__main__': + _test() diff --git a/examples/scripts/kagome/plot_lattice.py b/examples/scripts/kagome/plot_lattice.py new file mode 100644 index 0000000..eaaf53c --- /dev/null +++ b/examples/scripts/kagome/plot_lattice.py @@ -0,0 +1,81 @@ + +from matplotlib import pyplot as plt +import numpy as np + + +def plot_lattice(vertices, edges): + + f, ax = plt.subplots() + + # set up the plot + ax.set_aspect('equal') + ax.set_axis_off() + + ax.set_xlim( + -0.5 + min(x for x, _ in vertices), + 0.5 + max(x for x, _ in vertices) + ) + ax.set_ylim( + -0.5 + min(y for _, y in vertices), + 0.5 + max(y for _, y in vertices) + ) + + vertex_width = size_to_pts(5, f, ax) + bar_width = size_to_pts(0.1, f, ax) + + plt.scatter( + *zip(*vertices), + s=vertex_width, + c='white', + edgecolor='black', + linewidth=1.5 + ) + for idx, vertex in enumerate(vertices): + plt.text( + *vertex, str(idx), + ha='center', va='center_baseline', + fontweight='bold', + size=size_to_pts(0.18, f, ax) + ) + + for i, j in edges: + neighbors = are_neighbors(vertices[i], vertices[j]) + color = '0.5' if neighbors else '0.8' + plt.plot(*zip(vertices[i], vertices[j]), + color=color, + linewidth=bar_width, + zorder=0 if neighbors else -1) + + plt.show() + + +def are_neighbors(a, b): + return np.isclose(np.hypot(b[0]-a[0], b[1]-a[1]), 1) + + +def size_to_pts(size, fig, ax): + ''' + Convert a size in data units to a number of points, for use in + e.g. linewidth argument + ''' + ppd = 72./fig.dpi + trans = ax.transData.transform + return ((trans((size, 1))-trans((0, 0)))*ppd)[0] + + +def main(): + from sys import argv + from lattice_library import basis_to_graph, kagome_clusters + + if len(argv) < 2: + lattice_name = '12' + else: + lattice_name = argv[1] + + plot_lattice( + *basis_to_graph(kagome_clusters[lattice_name]) + ) + + +if __name__ == '__main__': + main() diff --git a/examples/scripts/kagome/run_kagome.py b/examples/scripts/kagome/run_kagome.py new file mode 100644 index 0000000..157ff9a --- /dev/null +++ b/examples/scripts/kagome/run_kagome.py @@ -0,0 +1,104 @@ + +from argparse import ArgumentParser +from datetime import datetime + +from dynamite.operators import sigmax, sigmay, sigmaz, op_sum +from dynamite.subspaces import SpinConserve, XParity +from dynamite.tools import mpi_print + +from lattice_library import kagome_clusters, basis_to_graph + + +def heisenberg(i, j): + ''' + The Heisenberg interaction between sites i and j. + ''' + # 0.25 to account for spin operators instead of Paulis + return op_sum(0.25*s(i)*s(j) for s in [sigmax, sigmay, sigmaz]) + + +def build_hamiltonian(cluster_name): + ''' + Build the nearest-neighbor Heisenberg interaction with J=1 for + the Kagome lattice on a torus, specified by the clusters in + lattice_library.py. + ''' + _, edges = basis_to_graph(kagome_clusters[cluster_name]) + return op_sum(heisenberg(i, j) for i, j in edges) + + +def compute_correlation_functions(state): + ''' + Compute the expectation values of various operators that are + useful for identifying spin liquids. + ''' + raise NotImplementedError() + + +def main(): + args = parse_args() + + mpi_print('Heisenberg interaction on the Kagome lattice') + mpi_print(f'Cluster: {args.cluster}') + mpi_print(f'Use shell matrices: {args.shell}') + + H = build_hamiltonian(args.cluster) + + # number of spins in the support of H + N = H.get_length() + + # total magnetization is conserved + subspace = SpinConserve(N, N//2) + + sector = None + if not args.no_z2: + # apply an extra Z2 symmetry on top if N is even + # the sector containing the ground state depends on the value of N % 4 + if N % 4 == 0: + sector = +1 + elif N % 4 == 2: + sector = -1 + + if sector is None: + mpi_print(f'Not applying XParity (Z2) subspace') + else: + mpi_print(f'XParity (Z2) symmetry sector: {sector}') + subspace = XParity(subspace, sector=sector) + + mpi_print() + + H.subspace = subspace + + H.shell = args.shell + + # time the eigsolve! + tick = datetime.now() + # eigsolve may return more than 2 values if it converges more + gs_energy, e1_energy = H.eigsolve(nev=2)[:2] + tock = datetime.now() + + mpi_print(f'Ground state energy E: {gs_energy}') + mpi_print(f'E/N: {gs_energy/N}') + mpi_print() + + gap = e1_energy-gs_energy + mpi_print(f'Gap: {gap}') + mpi_print(f'Gap/N: {gap/N}') + mpi_print() + mpi_print(f'Solve completed in {tock-tick}') + + +def parse_args(): + parser = ArgumentParser(description='Solve for the ground state energy of the Heisenberg model ' + 'on the Kagome lattice.') + + parser.add_argument('cluster', default='12', help='which Kagome cluster to use ' + '(see lattice_library.py)') + parser.add_argument('--shell', action='store_true', help='whether to use shell matrices') + parser.add_argument('--no-z2', action='store_true', help='do not apply XParity subspace') + + return parser.parse_args() + + +if __name__ == '__main__': + main() diff --git a/examples/tutorial/0-Welcome.ipynb b/examples/tutorial/0-Welcome.ipynb index 7e02038..5faed7b 100644 --- a/examples/tutorial/0-Welcome.ipynb +++ b/examples/tutorial/0-Welcome.ipynb @@ -13,7 +13,9 @@ "\n", "The dynamite documentation is online at [dynamite.readthedocs.io](https://dynamite.readthedocs.io/)---you may find it useful during these examples.\n", "\n", - "When you're ready, [head to notebook 1](1-Operators.ipynb) to jump in!" + "To complement this tutorial, there is also a set of [example scripts](https://github.com/GregDMeyer/dynamite/tree/master/examples/scripts) which demonstrate useful code patterns for working with dynamite, and can also serve as starting points for research projects.\n", + "\n", + "When you're ready to begin the tutorial, [head to notebook 1](1-Operators.ipynb) to jump in!" ] } ], @@ -33,7 +35,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/examples/tutorial/7-Conclusion.ipynb b/examples/tutorial/7-Conclusion.ipynb index 646138d..6048009 100644 --- a/examples/tutorial/7-Conclusion.ipynb +++ b/examples/tutorial/7-Conclusion.ipynb @@ -9,17 +9,9 @@ "\n", "You have completed a tour of dynamite's features! Hopefully you will find it useful. \n", "\n", - "Here are a few final things to explore:\n", - "\n", - " - While notebooks are a good sandbox for exploring, for serious computations dynamite is best used in a standalone Python script. This allows, for example, easy parallelization on clusters via MPI.\n", - " - Speaking of clusters, dynamite is super easy to run, no setup required, on a cluster with Singularity (or another containerization application) installed. On a compute node, you can simply run the following:\n", - " - `singularity exec docker://gdmeyer/dynamite python your_script.py`\n", - " - dynamite also supports GPU computations! Using the `gdmeyer/dynamite:latest-cuda` container, on a compute node with a GPU you can run\n", - " - `singularity exec --nv docker://gdmeyer/dynamite:latest-cuda python your_script.py`\n", - " - You may find it best to enable shell matrices, due to the limited RAM of most GPUs\n", - " - For more information about running dynamite on clusters with Singularity, see [the documentation](https://dynamite.readthedocs.io/en/latest/containers.html#singularity-usage).\n", + "You may be interested in now checking out dynamite's [example scripts](https://github.com/GregDMeyer/dynamite/tree/master/examples/scripts), which demonstrate useful code patterns for working with dynamite in practice, and can also serve as starting points for research projects. Try running one of the scripts in parallel with MPI, or on a GPU!\n", " \n", - "As a reminder, dynamite's documentation is at [dynamite.readthedocs.io](https://dynamite.readthedocs.io/)." + "As a reminder, dynamite's documentation is at [dynamite.readthedocs.io](https://dynamite.readthedocs.io/). Have fun!" ] } ], @@ -39,7 +31,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.2" } }, "nbformat": 4, From d44cf5ffa49622954437732f7ecd43f7ed704d91 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 21 Mar 2023 15:21:50 -0700 Subject: [PATCH 11/73] fix typo --- examples/scripts/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/scripts/README.md b/examples/scripts/README.md index ef1fd9d..deb3b0e 100644 --- a/examples/scripts/README.md +++ b/examples/scripts/README.md @@ -67,18 +67,18 @@ mpirun -n 4 python run_.py ``` On certain systems, like clusters, the command to launch MPI jobs may be different---check your system's documentation! -You can also try running the examples on a GPU, if you have access to one! +You can also try running the examples on a GPU, if you have access to one! If dynamite is compiled with GPU support it will perform computations on the GPU by default. The easiest way to access a dynamite build -that has been compiled with GPU support is probably via the Docker images; see [the documentation](https://dynamite.readthedocs.io/en/latest/containers.html) +that has been compiled with GPU support is probably via the Docker images; see [the documentation](https://dynamite.readthedocs.io/en/latest/containers.html) for details! ## Running in docker -Note that these examples are included in dynamite's docker images at `/home/dnm/examples/scripts/`, so you can easily try them out. For example, to run the +Note that these examples are included in dynamite's docker images at `/home/dnm/examples/scripts/`, so you can easily try them out. For example, to run the Kagome example with 21 spins you can simply do ```bash -docker run --rm -it gdmeyer/dynamite:latest python examples/kagome/run_kagome.py 21 +docker run --rm -it gdmeyer/dynamite:latest python examples/scripts/kagome/run_kagome.py 21 ``` Or to run it [on a GPU in a compute cluster using Singularity](https://dynamite.readthedocs.io/en/latest/containers.html#singularity-usage): From d1ee763f78dd776d3f405e07a03abf6aa709293c Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 22 Mar 2023 10:50:40 -0700 Subject: [PATCH 12/73] update SYK example --- examples/scripts/SYK/README.ipynb | 33 ++++++++++++++++++++----------- examples/scripts/SYK/README.md | 31 ++++++++++++++++++----------- examples/scripts/SYK/run_syk.py | 15 ++++++-------- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/examples/scripts/SYK/README.ipynb b/examples/scripts/SYK/README.ipynb index 366042e..1c16cfb 100644 --- a/examples/scripts/SYK/README.ipynb +++ b/examples/scripts/SYK/README.ipynb @@ -27,14 +27,12 @@ "source": [ "## Overview\n", "\n", - "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to exhibit *fast scrambling*, where quantum information is scrambled at the maximum possible rate. It is a particularly interesting system because it can be connected to the dynamics of quantum information in black holes, providing a testbed for surprising phenomena such as scrambling-based teleportation. The example code here mirrors closely a study which used dynamite to show numerical evidence of fast scrambling behavior in the SYK model.[1](#ref1)\n", - "TODO: I'd like to add several more references---is there a nice review paper that we could cite here?\n", + "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can also be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8)\n", "\n", "The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength:\n", "\n", - "$$H = \\frac{6}{N^3} \\sum_{ijkl} J_{ijkl} \\chi_i \\chi_j \\chi_k \\chi_l$$\n", - "\n", - "where $J_{ijkl}$ are random with some particular distribution (we will use the uniform distribution in the range $[-1, 1]$).\n", + "$$H = \\sqrt{\\frac{6}{N^3}} \\sum_{ijkl} J_{ijkl} \\chi_i \\chi_j \\chi_k \\chi_l$$\n", + "where $J_{ijkl}$ are randomly chosen from a Gaussian distribution with variance 1.\n", "\n", "To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \\lfloor i/2 \\rfloor$. Then\n", "$$\\chi_i = \\sigma^{\\{x, y\\}}_q \\prod_{m \\in [0, q-1]} \\sigma^z$$\n", @@ -100,7 +98,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "185 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + "180 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" ] } ], @@ -120,7 +118,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "1.99 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" + "1.98 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)\n" ] } ], @@ -135,8 +133,8 @@ "source": [ "## Goals\n", "\n", - "In this project we investigate the fast scrambling behavior of the SYK model by studying *out of time order correlators* (OTOCs). In particular, we will measure how much a system is \"scrambled\" at time $t$ by measuring to what extent two local operators $V(0)$ and $W(t)$ anticommute (where the anticommutator at time $t=0$ is zero). The quantity we will measure is\n", - "$$C(t) = \\langle \\{ W(t), V(0) \\}^2 \\rangle .$$\n", + "In this project we investigate the fast scrambling behavior of the SYK model by studying *out of time order correlators* (OTOCs). In particular, we will measure to what extent two local operators $V(0)$ and $W(t)$ anticommute for various times $t$, where the anticommutator at time $t=0$ is zero:\n", + "$$C(t) = \\langle \\left| \\lbrace W(t), V(0) \\rbrace \\right| ^2 \\rangle .$$\n", "It's helpful to reduce this to the following equivalent expression\n", "$$C(t) = 2 \\mathrm{Re}\\left[ \\langle W(t) V(0) W(t) V(0) \\rangle \\right] + 1/2$$\n", "which is the formulation of $C(t)$ that we will use in the computations here.\n", @@ -158,7 +156,7 @@ "source": [ "## Remark: matrix-free methods\n", "\n", - "The SYK Hamiltonian has a very large number of terms. Most Hamiltonians we encounter have a number of terms that scale perhaps as $L$ or $L^2$, where $L$ is the number of spins. The SYK model has roughly $(2L)^4$ terms. For example, with only 40 Majoranas there are already over 90,000 terms!" + "The SYK Hamiltonian has a very large number of terms. Most Hamiltonians we encounter have a number of terms that scale perhaps as $L$ or $L^2$, where $L$ is the number of spins. This SYK model has roughly $(2L)^4$ terms. For example, with only 40 Majoranas there are already over 90,000 terms!" ] }, { @@ -253,7 +251,9 @@ "id": "9065a455-d109-403c-9475-68bd3a785afc", "metadata": {}, "source": [ - "In general, the tradeoff is that performing computations matrix-free can be somewhat slower, due to the extra cost of computing the matrix elements. But this is not always true: for matrix-vector multiplications, the limiting factor is often the memory bandwidth, and having to pull less data from memory can actually speed things up. This is especially true when running things in parallel, if many CPUs are sharing the same memory bus. Ultimately, one should just experiment with different configurations and see which one gives the best combination of speed and memory cost." + "In general, the tradeoff is that performing computations matrix-free can be somewhat slower, due to the extra cost of computing the matrix elements. But this is not always true: for matrix-vector multiplications, the limiting factor is often the memory bandwidth, and having to pull less data from memory can actually speed things up. This is especially true when running things in parallel, if many CPUs are sharing the same memory bus. Ultimately, one should just experiment with different configurations and see which one gives the best combination of speed and memory cost.\n", + "\n", + "Finally, we note that it is worth thinking holistically about how to best handle difficult numerical problems like this. For example, it has been shown that \"sparse SYK,\" which has many fewer terms, can demonstrate a lot of the same physics with much lower memory and runtime costs.[9,10](#ref9)" ] }, { @@ -368,7 +368,16 @@ "source": [ "## References\n", "\n", - "1 [Kobrin et al., \"Many-Body Chaos in the Sachdev-Ye-Kitaev Model\"](https://doi.org/10.1103/PhysRevLett.126.030602) " + "1 [Kitaev, \"A simple model of quantum holography\"](https://online.kitp.ucsb.edu/online/entangled15/kitaev/) \n", + "2 [Maldacena and Stanford, \"Remarks on the Sachdev-Ye-Kitaev model\"](https://doi.org/10.1103/PhysRevD.94.106002) \n", + "3 [Maldacena et al., \"A bound on chaos\"](https://doi.org/10.1007/JHEP08(2016)106) \n", + "4 [Gao et al., \"Traversable wormholes via a double trace deformation\"](https://doi.org/10.1007/JHEP12(2017)151) \n", + "5 [Maldacena et al., \"Diving into traversable wormholes\"](https://doi.org/10.1002/prop.201700034) \n", + "6 [Brown et al., \"Quantum Gravity in the Lab: Teleportation by Size and Traversable Wormholes\"](https://doi.org/10.1103/PRXQuantum.4.010320) \n", + "7 [Schuster et al., \"Many-Body Quantum Teleportation via Operator Spreading in the Traversable Wormhole Protocol\"](https://doi.org/10.1103/PhysRevX.12.031013) \n", + "8 [Kobrin et al., \"Many-Body Chaos in the Sachdev-Ye-Kitaev Model\"](https://doi.org/10.1103/PhysRevLett.126.030602) \n", + "9 [Xu et al., \"A Sparse Model of Quantum Holography\"](https://doi.org/10.48550/arXiv.2008.02303) \n", + "10 [Cáceres et al., \"Sparse SYK and traversable wormholes\"](https://doi.org/10.1007/JHEP11(2021)015) " ] } ], diff --git a/examples/scripts/SYK/README.md b/examples/scripts/SYK/README.md index 0fc461d..36afb58 100644 --- a/examples/scripts/SYK/README.md +++ b/examples/scripts/SYK/README.md @@ -14,14 +14,12 @@ ## Overview -This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to exhibit *fast scrambling*, where quantum information is scrambled at the maximum possible rate. It is a particularly interesting system because it can be connected to the dynamics of quantum information in black holes, providing a testbed for surprising phenomena such as scrambling-based teleportation. The example code here mirrors closely a study which used dynamite to show numerical evidence of fast scrambling behavior in the SYK model.[1](#ref1) -TODO: I'd like to add several more references---is there a nice review paper that we could cite here? +This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can also be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8) The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength: -$$H = \frac{6}{N^3} \sum_{ijkl} J_{ijkl} \chi_i \chi_j \chi_k \chi_l$$ - -where $J_{ijkl}$ are random with some particular distribution (we will use the uniform distribution in the range $[-1, 1]$). +$$H = \sqrt{\frac{6}{N^3}} \sum_{ijkl} J_{ijkl} \chi_i \chi_j \chi_k \chi_l$$ +where $J_{ijkl}$ are randomly chosen from a Gaussian distribution with variance 1. To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \lfloor i/2 \rfloor$. Then $$\chi_i = \sigma^{\{x, y\}}_q \prod_{m \in [0, q-1]} \sigma^z$$ @@ -54,7 +52,7 @@ from run_syk import build_hamiltonian, build_hamiltonian_simple %timeit -n 1 -r 1 build_hamiltonian(N=16) ``` - 185 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) + 180 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) @@ -62,13 +60,13 @@ from run_syk import build_hamiltonian, build_hamiltonian_simple %timeit -n 1 -r 1 build_hamiltonian_simple(N=16) ``` - 1.99 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) + 1.98 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each) ## Goals -In this project we investigate the fast scrambling behavior of the SYK model by studying *out of time order correlators* (OTOCs). In particular, we will measure how much a system is "scrambled" at time $t$ by measuring to what extent two local operators $V(0)$ and $W(t)$ anticommute (where the anticommutator at time $t=0$ is zero). The quantity we will measure is -$$C(t) = \langle \{ W(t), V(0) \}^2 \rangle .$$ +In this project we investigate the fast scrambling behavior of the SYK model by studying *out of time order correlators* (OTOCs). In particular, we will measure to what extent two local operators $V(0)$ and $W(t)$ anticommute for various times $t$, where the anticommutator at time $t=0$ is zero: +$$C(t) = \langle \left| \lbrace W(t), V(0) \rbrace \right| ^2 \rangle .$$ It's helpful to reduce this to the following equivalent expression $$C(t) = 2 \mathrm{Re}\left[ \langle W(t) V(0) W(t) V(0) \rangle \right] + 1/2$$ which is the formulation of $C(t)$ that we will use in the computations here. @@ -84,7 +82,7 @@ The function `compute_otoc` starts with $\left| \psi_r \right>$ and just works t ## Remark: matrix-free methods -The SYK Hamiltonian has a very large number of terms. Most Hamiltonians we encounter have a number of terms that scale perhaps as $L$ or $L^2$, where $L$ is the number of spins. The SYK model has roughly $(2L)^4$ terms. For example, with only 40 Majoranas there are already over 90,000 terms! +The SYK Hamiltonian has a very large number of terms. Most Hamiltonians we encounter have a number of terms that scale perhaps as $L$ or $L^2$, where $L$ is the number of spins. This SYK model has roughly $(2L)^4$ terms. For example, with only 40 Majoranas there are already over 90,000 terms! ```python @@ -130,6 +128,8 @@ H.estimate_memory() In general, the tradeoff is that performing computations matrix-free can be somewhat slower, due to the extra cost of computing the matrix elements. But this is not always true: for matrix-vector multiplications, the limiting factor is often the memory bandwidth, and having to pull less data from memory can actually speed things up. This is especially true when running things in parallel, if many CPUs are sharing the same memory bus. Ultimately, one should just experiment with different configurations and see which one gives the best combination of speed and memory cost. +Finally, we note that it is worth thinking holistically about how to best handle difficult numerical problems like this. For example, it has been shown that "sparse SYK," which has many fewer terms, can demonstrate a lot of the same physics with much lower memory and runtime costs.[9,10](#ref9) + ## Remark: disorder realizations and parallelism (also discussed in MBL example) @@ -195,4 +195,13 @@ Try running this computation with MPI, or on a GPU if you have one, and compare ## References -1 [Kobrin et al., "Many-Body Chaos in the Sachdev-Ye-Kitaev Model"](https://doi.org/10.1103/PhysRevLett.126.030602) +1 [Kitaev, "A simple model of quantum holography"](https://online.kitp.ucsb.edu/online/entangled15/kitaev/) +2 [Maldacena and Stanford, "Remarks on the Sachdev-Ye-Kitaev model"](https://doi.org/10.1103/PhysRevD.94.106002) +3 [Maldacena et al., "A bound on chaos"](https://doi.org/10.1007/JHEP08(2016)106) +4 [Gao et al., "Traversable wormholes via a double trace deformation"](https://doi.org/10.1007/JHEP12(2017)151) +5 [Maldacena et al., "Diving into traversable wormholes"](https://doi.org/10.1002/prop.201700034) +6 [Brown et al., "Quantum Gravity in the Lab: Teleportation by Size and Traversable Wormholes"](https://doi.org/10.1103/PRXQuantum.4.010320) +7 [Schuster et al., "Many-Body Quantum Teleportation via Operator Spreading in the Traversable Wormhole Protocol"](https://doi.org/10.1103/PhysRevX.12.031013) +8 [Kobrin et al., "Many-Body Chaos in the Sachdev-Ye-Kitaev Model"](https://doi.org/10.1103/PhysRevLett.126.030602) +9 [Xu et al., "A Sparse Model of Quantum Holography"](https://doi.org/10.48550/arXiv.2008.02303) +10 [Cáceres et al., "Sparse SYK and traversable wormholes"](https://doi.org/10.1007/JHEP11(2021)015) diff --git a/examples/scripts/SYK/run_syk.py b/examples/scripts/SYK/run_syk.py index 43bae14..673a8cc 100644 --- a/examples/scripts/SYK/run_syk.py +++ b/examples/scripts/SYK/run_syk.py @@ -2,7 +2,7 @@ from itertools import combinations from argparse import ArgumentParser from sys import stderr -from numpy import random +import numpy as np from dynamite import config from dynamite.operators import op_sum, op_product @@ -28,7 +28,7 @@ def main(): else: seed = args.seed mpi_print(f' seed, {seed}', file=stderr) - random.seed(seed) + np.random.seed(seed) # extra newline for readability of the output mpi_print(file=stderr) @@ -39,9 +39,6 @@ def main(): # globally set the number of spins to ceil(N/2) config.L = (args.N+1)//2 - # ensures we get the same random numbers on all MPI ranks - random.seed(args.seed) - # the Hamiltonian conserves spin parity in the Z basis (Z2 symmetry) # but the majorana operators W and V take us between the two symmetry sectors # so we will make use of both parity subspaces (see below) @@ -120,10 +117,10 @@ def build_hamiltonian_simple(N): for j in range(i+1, N): for k in range(j+1, N): for l in range(k+1, N): - Jijkl = random.uniform(-1, 1) + Jijkl = np.random.normal() H += Jijkl*majorana(i)*majorana(j)*majorana(k)*majorana(l) - return 6/N**3 * H + return np.sqrt(6/N**3) * H def build_hamiltonian(N): @@ -145,7 +142,7 @@ def gen_products(N): p = op_product(majoranas[idx] for idx in idxs) # random value is the same on each rank because we set the seed explicitly in main() - Jijkl = random.uniform(-1, 1) + Jijkl = np.random.normal() # using scale() is faster than doing 'Jijkl*p' because it does not create # a new operator object, instead just scaling the coeffs of the existing one @@ -160,7 +157,7 @@ def gen_products(N): # global prefactor # again, using scale() is much more efficient than "return 6/N**3 * H" # because it doesn't create a copy of the whole gigantic Hamiltonian - H.scale(6/N**3) + H.scale(np.sqrt(6/N**3)) return H From 79b602bbe0bbbb0bf74dae3e8832422d4c368821 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 22 Mar 2023 10:56:38 -0700 Subject: [PATCH 13/73] fix broken equation --- examples/scripts/SYK/README.ipynb | 4 ++-- examples/scripts/SYK/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/scripts/SYK/README.ipynb b/examples/scripts/SYK/README.ipynb index 1c16cfb..49f41c1 100644 --- a/examples/scripts/SYK/README.ipynb +++ b/examples/scripts/SYK/README.ipynb @@ -27,7 +27,7 @@ "source": [ "## Overview\n", "\n", - "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can also be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8)\n", + "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8)\n", "\n", "The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength:\n", "\n", @@ -35,7 +35,7 @@ "where $J_{ijkl}$ are randomly chosen from a Gaussian distribution with variance 1.\n", "\n", "To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \\lfloor i/2 \\rfloor$. Then\n", - "$$\\chi_i = \\sigma^{\\{x, y\\}}_q \\prod_{m \\in [0, q-1]} \\sigma^z$$\n", + "$$\\chi_i = \\sigma^{\\lbrace x, y\\rbrace}_q \\prod_{m \\in [0, q-1]} \\sigma^z$$\n", "where the first Pauli is $\\sigma^x$ if $i$ is even and $\\sigma^y$ if it's odd. In words, the Majorana consists of a $\\sigma^x$ or $\\sigma^y$ with a string of $\\sigma^z$ extending to the edge of the spin chain. Note that we get two Majoranas for each spin!\n", "\n", "This is straightforward to implement in dynamite, but is actually already built in in the `dynamite.extras` module so we don't have to do it ourselves:" diff --git a/examples/scripts/SYK/README.md b/examples/scripts/SYK/README.md index 36afb58..6fb1763 100644 --- a/examples/scripts/SYK/README.md +++ b/examples/scripts/SYK/README.md @@ -14,7 +14,7 @@ ## Overview -This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can also be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8) +This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8) The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength: @@ -22,7 +22,7 @@ $$H = \sqrt{\frac{6}{N^3}} \sum_{ijkl} J_{ijkl} \chi_i \chi_j \chi_k \chi_l$$ where $J_{ijkl}$ are randomly chosen from a Gaussian distribution with variance 1. To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \lfloor i/2 \rfloor$. Then -$$\chi_i = \sigma^{\{x, y\}}_q \prod_{m \in [0, q-1]} \sigma^z$$ +$$\chi_i = \sigma^{\lbrace x, y\rbrace}_q \prod_{m \in [0, q-1]} \sigma^z$$ where the first Pauli is $\sigma^x$ if $i$ is even and $\sigma^y$ if it's odd. In words, the Majorana consists of a $\sigma^x$ or $\sigma^y$ with a string of $\sigma^z$ extending to the edge of the spin chain. Note that we get two Majoranas for each spin! This is straightforward to implement in dynamite, but is actually already built in in the `dynamite.extras` module so we don't have to do it ourselves: From 1701d572dadd7bad13363bf19b877aa98abcc5cc Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 22 Mar 2023 11:05:50 -0700 Subject: [PATCH 14/73] really fix broken equation --- examples/scripts/SYK/README.ipynb | 4 +++- examples/scripts/SYK/README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/scripts/SYK/README.ipynb b/examples/scripts/SYK/README.ipynb index 49f41c1..c75519c 100644 --- a/examples/scripts/SYK/README.ipynb +++ b/examples/scripts/SYK/README.ipynb @@ -35,7 +35,9 @@ "where $J_{ijkl}$ are randomly chosen from a Gaussian distribution with variance 1.\n", "\n", "To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \\lfloor i/2 \\rfloor$. Then\n", - "$$\\chi_i = \\sigma^{\\lbrace x, y\\rbrace}_q \\prod_{m \\in [0, q-1]} \\sigma^z$$\n", + "\n", + "$$\\chi_i = \\sigma^{\\lbrace x, y\\rbrace}_q \\prod\\limits_{m \\in [0, q-1]} \\sigma^z_m$$\n", + "\n", "where the first Pauli is $\\sigma^x$ if $i$ is even and $\\sigma^y$ if it's odd. In words, the Majorana consists of a $\\sigma^x$ or $\\sigma^y$ with a string of $\\sigma^z$ extending to the edge of the spin chain. Note that we get two Majoranas for each spin!\n", "\n", "This is straightforward to implement in dynamite, but is actually already built in in the `dynamite.extras` module so we don't have to do it ourselves:" diff --git a/examples/scripts/SYK/README.md b/examples/scripts/SYK/README.md index 6fb1763..8888688 100644 --- a/examples/scripts/SYK/README.md +++ b/examples/scripts/SYK/README.md @@ -22,7 +22,9 @@ $$H = \sqrt{\frac{6}{N^3}} \sum_{ijkl} J_{ijkl} \chi_i \chi_j \chi_k \chi_l$$ where $J_{ijkl}$ are randomly chosen from a Gaussian distribution with variance 1. To map the Majoranas onto the spin systems that are natively supported in dynamite, we can use the following transformation. For the Majorana with index $i$, let $q = \lfloor i/2 \rfloor$. Then -$$\chi_i = \sigma^{\lbrace x, y\rbrace}_q \prod_{m \in [0, q-1]} \sigma^z$$ + +$$\chi_i = \sigma^{\lbrace x, y\rbrace}_q \prod\limits_{m \in [0, q-1]} \sigma^z_m$$ + where the first Pauli is $\sigma^x$ if $i$ is even and $\sigma^y$ if it's odd. In words, the Majorana consists of a $\sigma^x$ or $\sigma^y$ with a string of $\sigma^z$ extending to the edge of the spin chain. Note that we get two Majoranas for each spin! This is straightforward to implement in dynamite, but is actually already built in in the `dynamite.extras` module so we don't have to do it ourselves: From 5c3e556260a0d8af2dfa4b74ae99e63ee2c06e80 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 22 Mar 2023 11:10:03 -0700 Subject: [PATCH 15/73] add example scripts to CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ffcb37..a68044d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## 0.3.2 - IN PROGRESS ### Added + - Detailed example scripts (in `examples/scripts`) - `Operator.expectation()`, convenience function to compute the expectation value of the operator with respect to a state - `dynamite.tools.MPI_COMM_WORLD()` which returns PETSc's MPI communicator object From 166c17a7c3cbb8a345566029b974be8cdf7a479a Mon Sep 17 00:00:00 2001 From: Greg Meyer Date: Wed, 22 Mar 2023 11:11:57 -0700 Subject: [PATCH 16/73] update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb7bd37..9b45f9e 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,5 @@ Dynamite [![Documentation Status](https://readthedocs.org/projects/dynamite/badge/?version=latest)](https://dynamite.readthedocs.io/en/latest/?badge=latest) -Welcome to `dynamite`, which provides fast, massively parallel evolution and eigensolving for spin chain Hamiltonians. It uses the PETSc/SLEPc libraries as a backend. Visit the [ReadTheDocs](https://dynamite.readthedocs.io)! +Welcome to `dynamite`, which provides fast, massively parallel evolution and eigensolving for spin chain Hamiltonians. +Visit the [ReadTheDocs](https://dynamite.readthedocs.io)! From 614b379775bcdcbcbaadbeeec7891d9a616f01c0 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 21 Apr 2023 14:53:53 -0700 Subject: [PATCH 17/73] bump petsc/slepc to 3.19 and CUDA to 12 --- docker/Dockerfile | 18 +++++++++--------- docs/install.rst | 4 ++-- petsc_config/cuda-opt.py | 7 ++----- pyproject.toml | 4 ++-- setup.py | 4 ++-- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index cbafd8a..b7ef4cf 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,9 @@ ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' -ARG PETSC_VERSION=3.18.4 -ARG SLEPC_VERSION=3.18.2 -ARG CUDA_VERSION=11.8.0 +ARG PETSC_VERSION=3.19.0 +ARG SLEPC_VERSION=3.19.0 +ARG CUDA_VERSION=12.1.0 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 @@ -11,7 +11,7 @@ ARG CPU_PYTHON_VERSION=3.11 FROM python:${CPU_PYTHON_VERSION} AS base-cpu # install dependencies -ONBUILD RUN apt-get update && \ +RUN apt-get update && \ \ DEBIAN_FRONTEND="noninteractive" \ apt-get install -y --no-install-recommends \ @@ -35,7 +35,7 @@ ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --download-mumps=0 --with-mumps=1" FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${CUDA_UBUNTU_VERSION} AS base-gpu # install dependencies -ONBUILD RUN apt-get update && \ +RUN apt-get update && \ \ DEBIAN_FRONTEND="noninteractive" \ apt-get install -y --no-install-recommends \ @@ -54,8 +54,9 @@ ONBUILD RUN apt-get update && \ ARG BUILD_TYPE ENV PETSC_ARCH=cuda-$BUILD_TYPE +ARG CUDA_ARCH=all ARG PETSC_CONFIG_FLAGS -ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --with-mpi=0" +ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --with-mpi=0 --with-cuda-arch=$CUDA_ARCH" FROM base-${PLATFORM} as build @@ -90,8 +91,7 @@ USER dnm WORKDIR /opt/petsc ENV PETSC_DIR=/opt/petsc COPY --chown=dnm:dnm petsc_config/$PETSC_ARCH.py . -ARG CUDA_ARCH=70 -RUN DNM_CUDA_ARCH=$CUDA_ARCH ./$PETSC_ARCH.py ${PETSC_CONFIG_FLAGS} && \ +RUN ./$PETSC_ARCH.py ${PETSC_CONFIG_FLAGS} && \ make all @@ -156,7 +156,7 @@ ENV OMP_NUM_THREADS=1 FROM nvidia/cuda:${CUDA_VERSION}-runtime-ubuntu${CUDA_UBUNTU_VERSION} AS release-base-gpu -ONBUILD RUN apt-get update && \ +RUN apt-get update && \ \ DEBIAN_FRONTEND="noninteractive" \ apt-get install -y --no-install-recommends \ diff --git a/docs/install.rst b/docs/install.rst index a648eb1..c92f269 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -36,7 +36,7 @@ following. There is a configuration script that comes with dynamite which should .. code:: bash - git clone --depth 1 --branch v3.18.4 https://gitlab.com/petsc/petsc.git petsc + git clone --depth 1 --branch v3.19.0 https://gitlab.com/petsc/petsc.git petsc cd petsc python /petsc_config/complex-opt.py @@ -61,7 +61,7 @@ Now download and install SLEPc: .. code:: bash - git clone --depth 1 --branch v3.18.2 https://gitlab.com/slepc/slepc.git slepc + git clone --depth 1 --branch v3.19.0 https://gitlab.com/slepc/slepc.git slepc cd slepc ./configure diff --git a/petsc_config/cuda-opt.py b/petsc_config/cuda-opt.py index f363676..a94ec11 100755 --- a/petsc_config/cuda-opt.py +++ b/petsc_config/cuda-opt.py @@ -33,13 +33,10 @@ # may need/want to adjust to match your hardware's compute capability, # e.g. '80' for compute capability 8.0 - # can also adjust with DNM_CUDA_ARCH environment variable (see below) - '--with-cuda-arch': '75', + # can also supply a comma-separated list + #'--with-cuda-arch': '75', } -if 'DNM_CUDA_ARCH' in environ: - configure_option_dict['--with-cuda-arch'] = environ['DNM_CUDA_ARCH'] - if __name__ == '__main__': import sys import os diff --git a/pyproject.toml b/pyproject.toml index 69aee38..2db3f7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ "numpy", "scipy", "threadpoolctl", - "petsc4py == 3.18.4", - "slepc4py == 3.18.2", + "petsc4py == 3.19.0", + "slepc4py == 3.19.0", ] [build-system] diff --git a/setup.py b/setup.py index 9d1f862..3621d46 100644 --- a/setup.py +++ b/setup.py @@ -33,11 +33,11 @@ def get_cython_includes(): return [ os.path.join( os.environ['PETSC_DIR'], - 'src/binding/petsc4py/src/include' + 'src/binding/petsc4py/src' ), os.path.join( os.environ['SLEPC_DIR'], - 'src/binding/slepc4py/src/include' + 'src/binding/slepc4py/src' ) ] From 6d320ec8a0f2c67675ed9f73df708980389dd25e Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 24 Apr 2023 10:29:43 -0700 Subject: [PATCH 18/73] fix docker build of petsc4py --- docker/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b7ef4cf..f093573 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -72,7 +72,7 @@ USER dnm # activate venv ENV VIRTUAL_ENV=/venv ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN pip3 install --no-cache-dir --upgrade pip +RUN pip3 install --no-cache-dir --upgrade pip wheel from build as petsc @@ -121,6 +121,7 @@ RUN ./configure && make from petsc as dynamite-build +RUN pip3 install --no-cache-dir numpy RUN pip3 install --no-cache-dir $PETSC_DIR/src/binding/petsc4py COPY --from=slepc /opt/slepc /opt/slepc From b26518047b1ca038c81d1aa5f881282242f57028 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 24 Apr 2023 15:14:33 -0700 Subject: [PATCH 19/73] incorporate comments on example readmes --- examples/scripts/SYK/README.ipynb | 4 ++-- examples/scripts/SYK/README.md | 2 +- examples/scripts/floquet/README.ipynb | 6 +++--- examples/scripts/floquet/README.md | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/scripts/SYK/README.ipynb b/examples/scripts/SYK/README.ipynb index c75519c..4bac62f 100644 --- a/examples/scripts/SYK/README.ipynb +++ b/examples/scripts/SYK/README.ipynb @@ -27,7 +27,7 @@ "source": [ "## Overview\n", "\n", - "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8)\n", + "This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information highly efficiently via chaotic many-body dynamics.[1,2](#ref1) Indeed, it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes the rate of chaos, saturates its upper bound of $2\\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8)\n", "\n", "The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength:\n", "\n", @@ -399,7 +399,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/scripts/SYK/README.md b/examples/scripts/SYK/README.md index 8888688..cf863f7 100644 --- a/examples/scripts/SYK/README.md +++ b/examples/scripts/SYK/README.md @@ -14,7 +14,7 @@ ## Overview -This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information at the maximum possible rate.[1,2](#ref1) Furthermore it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes how rapidly chaotic trajectories diverge, saturates its upper bound of $2\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8) +This example explores the Sachdev-Ye-Kitaev (SYK) model. In spirit it represents the opposite of the localization explored in the MBL example: it is expected to scramble information highly efficiently via chaotic many-body dynamics.[1,2](#ref1) Indeed, it exhibits *maximal chaos*: the Lyapunov exponent, which characterizes the rate of chaos, saturates its upper bound of $2\pi T$, where $T$ is the temperature of the system.[3](#ref3) Its physics can be connected to the dynamics of quantum information in black holes, providing a testbed for exotic phenomena such as scrambling-based teleportation.[4,5,6,7](#ref4) The example code here mirrors closely a study which used dynamite to show numerical evidence for many-body chaos and gravitational dynamics in the SYK model.[8](#ref8) The SYK model gives us a chance to look at how quantum systems other than spins can be explored with dynamite, by transforming them onto a spin system. The SYK model we'll use consists of Majoranas interacting in 0D, with random couplings. Specifically it consists of every possible 4-body interaction among N Majoranas, with each term having a random coupling strength: diff --git a/examples/scripts/floquet/README.ipynb b/examples/scripts/floquet/README.ipynb index 099d380..27c63c2 100644 --- a/examples/scripts/floquet/README.ipynb +++ b/examples/scripts/floquet/README.ipynb @@ -21,7 +21,7 @@ "In this project we will track the time evolution of various states under a time-dependent Floquet Hamiltonian. The quantum system we analyze is physically interesting for a number of reasons, not least of which that it can exhibit Floquet prethermalization,[1](#ref1) which can support out-of-equilibrium phases of matter like time crystals! [2](#ref2)\n", "\n", "The specific model we will implement is the following. The 1D spin chain will evolve under a long range $ZZ$ interaction decaying as a power law, along with a nearest-neighbor $XX$ interaction and a uniform, static magnetic field $\\vec{h}$:\n", - "$$H = J_z \\sum_{i,j} \\frac{\\sigma^z_i \\sigma^z_j}{|i-j|^\\alpha} + J_x \\sum_{\\langle i, j \\rangle} \\sigma^x_i + \\sum_i \\vec{h} \\cdot \\vec{\\sigma}$$\n", + "$$H = J_z \\sum_{i1](#ref1) which can support out-of-equilibrium phases of matter like time crystals! [2](#ref2) The specific model we will implement is the following. The 1D spin chain will evolve under a long range $ZZ$ interaction decaying as a power law, along with a nearest-neighbor $XX$ interaction and a uniform, static magnetic field $\vec{h}$: -$$H = J_z \sum_{i,j} \frac{\sigma^z_i \sigma^z_j}{|i-j|^\alpha} + J_x \sum_{\langle i, j \rangle} \sigma^x_i + \sum_i \vec{h} \cdot \vec{\sigma}$$ +$$H = J_z \sum_{i Date: Mon, 24 Apr 2023 21:43:23 -0700 Subject: [PATCH 20/73] multi-arch cuda build --- docker/Dockerfile | 2 +- docker/build.py | 85 ++++++++++++++++++++--------------------------- 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index f093573..c98a166 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' ARG PETSC_VERSION=3.19.0 ARG SLEPC_VERSION=3.19.0 -ARG CUDA_VERSION=12.1.0 +ARG CUDA_VERSION=11.8.0 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 diff --git a/docker/build.py b/docker/build.py index f081d55..71fee7a 100644 --- a/docker/build.py +++ b/docker/build.py @@ -38,9 +38,8 @@ def parse_args(argv=None): parser.add_argument("--debug", action='store_true', help='Build in debug mode instead of release mode.') - parser.add_argument("--cuda-archs", type=lambda x: x.split(','), - default=['70', '80'], - help='CUDA compute capabilities.') + parser.add_argument("--cuda-arch", default='60,61,70,75,80,86', + help='CUDA compute capability (or comma-separated list of several).') parser.add_argument("--int-sizes", type=lambda x: [int(v) for v in x.split(',')], default=[32, 64], @@ -105,65 +104,53 @@ def remove_build_files(dir_to_remove): builds = [] for platform in args.platform: - if platform == 'gpu': - cuda_archs = args.cuda_archs - else: - cuda_archs = [None] - - for cuda_arch in cuda_archs: - for int_size in args.int_sizes: - - # this configuration is currently not supported - if platform == 'gpu' and int_size == 64: - print('Skipping unsupported 64-bit GPU build') - continue + for int_size in args.int_sizes: - tags = ["latest", version] + # this configuration is currently not supported + if platform == 'gpu' and int_size == 64: + print('Skipping unsupported 64-bit GPU build') + continue - if platform == 'gpu': - tags = [tag+'-cuda' for tag in tags] - no_cc_tags = tags.copy() - tags = [tag+'.cc'+cuda_arch for tag in tags] + tags = ["latest", version] - # default is cc 7.0 - if cuda_arch == '70': - tags = no_cc_tags + tags + if platform == 'gpu': + tags = [tag+'-cuda' for tag in tags] - cmd = [ - "docker", "build", - "--build-arg", f"PLATFORM={platform}", - "-f", "docker/Dockerfile", - "--target", target - ] + cmd = [ + "docker", "build", + "--build-arg", f"PLATFORM={platform}", + "-f", "docker/Dockerfile", + "--target", target + ] - if cuda_arch is not None: - cmd += ["--build-arg", f"CUDA_ARCH={cuda_arch}"] + if args.cuda_arch is not None: + cmd += ["--build-arg", f"CUDA_ARCH={args.cuda_arch}"] - if int_size == 64: - cmd += ["--build-arg", "PETSC_CONFIG_FLAGS=--with-64-bit-indices"] - tags = [tag+'-int64' for tag in tags] - elif int_size != 32: - raise ValueError(f"Unknown int size '{int_size}'") + if int_size == 64: + cmd += ["--build-arg", "PETSC_CONFIG_FLAGS=--with-64-bit-indices"] + tags = [tag+'-int64' for tag in tags] + elif int_size != 32: + raise ValueError(f"Unknown int size '{int_size}'") - if args.fresh and first_target: - cmd += ["--no-cache", "--pull"] + if args.fresh and first_target: + cmd += ["--no-cache", "--pull"] - if args.debug: - cmd += ["--build-arg", "BUILD_TYPE=debug"] + if args.debug: + cmd += ["--build-arg", "BUILD_TYPE=debug"] - if target == 'jupyter': - tags = [tag+'-jupyter' for tag in tags] + if target == 'jupyter': + tags = [tag+'-jupyter' for tag in tags] - for tag in tags: - cmd += ["-t", f"gdmeyer/dynamite:{tag}"] + for tag in tags: + cmd += ["-t", f"gdmeyer/dynamite:{tag}"] - cmd += ["."] + cmd += ["."] - build_dict = {} - build_dict['cmd'] = cmd - build_dict['tags'] = tags + build_dict = {} + build_dict['cmd'] = cmd + build_dict['tags'] = tags - builds.append(build_dict) + builds.append(build_dict) if not args.no_parallel: build_parallel(builds, build_dir, args) From 9dd9dcf3900a875bf55ef73b5840931b1a37d8bf Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 26 Apr 2023 12:22:59 -0700 Subject: [PATCH 21/73] fix less than not rendering correctly --- examples/scripts/floquet/README.ipynb | 2 +- examples/scripts/floquet/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/scripts/floquet/README.ipynb b/examples/scripts/floquet/README.ipynb index 27c63c2..d39157c 100644 --- a/examples/scripts/floquet/README.ipynb +++ b/examples/scripts/floquet/README.ipynb @@ -21,7 +21,7 @@ "In this project we will track the time evolution of various states under a time-dependent Floquet Hamiltonian. The quantum system we analyze is physically interesting for a number of reasons, not least of which that it can exhibit Floquet prethermalization,[1](#ref1) which can support out-of-equilibrium phases of matter like time crystals! [2](#ref2)\n", "\n", "The specific model we will implement is the following. The 1D spin chain will evolve under a long range $ZZ$ interaction decaying as a power law, along with a nearest-neighbor $XX$ interaction and a uniform, static magnetic field $\\vec{h}$:\n", - "$$H = J_z \\sum_{i1](#ref1) which can support out-of-equilibrium phases of matter like time crystals! [2](#ref2) The specific model we will implement is the following. The 1D spin chain will evolve under a long range $ZZ$ interaction decaying as a power law, along with a nearest-neighbor $XX$ interaction and a uniform, static magnetic field $\vec{h}$: -$$H = J_z \sum_{i Date: Wed, 26 Apr 2023 14:55:29 -0700 Subject: [PATCH 22/73] add note to examples about EE only on rank 0 --- examples/scripts/MBL/run_mbl.py | 2 ++ examples/scripts/floquet/run_floquet.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/examples/scripts/MBL/run_mbl.py b/examples/scripts/MBL/run_mbl.py index dddf3bc..42315ea 100644 --- a/examples/scripts/MBL/run_mbl.py +++ b/examples/scripts/MBL/run_mbl.py @@ -85,6 +85,8 @@ def print_eig_stats(evals, evecs, h, energy_point): for the provided eigenvalues and eigenstates ''' # sum the entropy for all evecs then divide by nev for the mean + # NOTE: entanglement_entropy returns the EE value only on MPI rank 0, and -1 on all other ranks. + # this is OK here because mpi_print below only prints on rank 0 entropy = sum(entanglement_entropy(v, keep=range(config.L//2)) for v in evecs) entropy /= len(evecs) diff --git a/examples/scripts/floquet/run_floquet.py b/examples/scripts/floquet/run_floquet.py index 6a0ed0e..75725a8 100644 --- a/examples/scripts/floquet/run_floquet.py +++ b/examples/scripts/floquet/run_floquet.py @@ -100,6 +100,8 @@ def print_stats(state, t, tmp, Deff, Sz_ops): Deff_energy = Deff.expectation(state, tmp_state=tmp) # half-chain entanglement entropy + # NOTE: entanglement_entropy returns the EE value only on MPI rank 0, and -1 on all other ranks. + # this is OK here because mpi_print below only prints on rank 0 entropy = entanglement_entropy(state, keep=range(config.L//2)) # Sz expectation values for each spin From 92ffc9c236c2a1967da9aaf985c961fe0014257b Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 27 Apr 2023 13:17:35 -0700 Subject: [PATCH 23/73] better comments in floquet example --- examples/scripts/floquet/run_floquet.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/scripts/floquet/run_floquet.py b/examples/scripts/floquet/run_floquet.py index 75725a8..76dfc50 100644 --- a/examples/scripts/floquet/run_floquet.py +++ b/examples/scripts/floquet/run_floquet.py @@ -74,15 +74,22 @@ def main(): def build_hamiltonian(alpha, Jz, Jx, h): # sums over all ranges of interaction - # index_sum takes the interaction sigmaz(0)*sigmaz(r) and translates it along the spin chain + # index_sum takes the interaction sigmaz(0)*sigmaz(r) and + # translates it along the spin chain long_range_ZZ = op_sum( 1/r**alpha * index_sum(0.25*sigmaz(0)*sigmaz(r)) for r in range(1, config.L) ) + # an XX interaction on every neighboring pair of sites + # the 0.25 is because spin operators are 1/2 times the Pauli nearest_neighbor_XX = index_sum(0.25*sigmax(0)*sigmax(1)) - magnetic_field = index_sum(op_sum(hi*0.5*s() for hi, s in zip(h, [sigmax, sigmay, sigmaz]))) + # op_sum combines the three components of the magnetic field vector, and then + # index_sum translates the resulting operator to every site along the spin chain + magnetic_field = index_sum( + op_sum(hi*0.5*s() for hi, s in zip(h, [sigmax, sigmay, sigmaz])) + ) return Jz*long_range_ZZ + Jx*nearest_neighbor_XX + magnetic_field From 949c9d87607a61581c8bae81632756d755919b73 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 27 Apr 2023 13:57:59 -0700 Subject: [PATCH 24/73] enable real-number docker builds --- docker/Dockerfile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c98a166..358c491 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -6,6 +6,7 @@ ARG SLEPC_VERSION=3.19.0 ARG CUDA_VERSION=11.8.0 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 +ARG SCALAR_TYPE=complex FROM python:${CPU_PYTHON_VERSION} AS base-cpu @@ -28,8 +29,9 @@ RUN apt-get update && \ ARG BUILD_TYPE ENV PETSC_ARCH=complex-$BUILD_TYPE +ARG SCALAR_TYPE ARG PETSC_CONFIG_FLAGS -ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --download-mumps=0 --with-mumps=1" +ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --download-mumps=0 --with-mumps=1 --with-scalar-type=$SCALAR_TYPE" FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${CUDA_UBUNTU_VERSION} AS base-gpu @@ -55,8 +57,9 @@ ARG BUILD_TYPE ENV PETSC_ARCH=cuda-$BUILD_TYPE ARG CUDA_ARCH=all +ARG SCALAR_TYPE ARG PETSC_CONFIG_FLAGS -ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --with-mpi=0 --with-cuda-arch=$CUDA_ARCH" +ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --with-mpi=0 --with-cuda-arch=$CUDA_ARCH --with-scalar-type=$SCALAR_TYPE" FROM base-${PLATFORM} as build From 7e0830a70d8f48fd7955f28934b5ba518e434d5d Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 27 Apr 2023 15:28:20 -0700 Subject: [PATCH 25/73] increase GPU launch grid size --- src/dynamite/_backend/bcuda_template_private.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dynamite/_backend/bcuda_template_private.h b/src/dynamite/_backend/bcuda_template_private.h index ec195ed..c261a57 100644 --- a/src/dynamite/_backend/bcuda_template_private.h +++ b/src/dynamite/_backend/bcuda_template_private.h @@ -4,8 +4,8 @@ #include #include "bcuda_template.h" -#define GPU_BLOCK_SIZE 128 -#define GPU_BLOCK_NUM 128 +#define GPU_BLOCK_SIZE 1024 +#define GPU_BLOCK_NUM 1024 PetscErrorCode C(BuildContext_CUDA,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( const msc_t *msc, From 442fa343186470c6c45c925140c0644fc0e1a300 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 28 Apr 2023 12:26:50 -0700 Subject: [PATCH 26/73] skip out-of-subspace terms earlier, in GPU shell --- src/dynamite/_backend/bcuda_template.cu | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/dynamite/_backend/bcuda_template.cu b/src/dynamite/_backend/bcuda_template.cu index a0274f3..a9b93fe 100644 --- a/src/dynamite/_backend/bcuda_template.cu +++ b/src/dynamite/_backend/bcuda_template.cu @@ -188,6 +188,12 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( for (mask_idx = 0; mask_idx < nmasks; ++mask_idx) { tmp = 0; bra = ket ^ masks[mask_idx]; + + col_idx = C(S2I_CUDA,RIGHT_SUBSPACE)(bra, right_subspace_data); + if (col_idx == -1) { // state is outside of the subspace; skip it + continue; + } + /* sum all terms for this matrix element */ for (term_idx = mask_offsets[mask_idx]; term_idx < mask_offsets[mask_idx+1]; ++term_idx) { #if defined(PETSC_USE_64BIT_INDICES) @@ -203,12 +209,7 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( add_imag(&tmp, sign * real_coeffs[term_idx]); } } - - col_idx = C(S2I_CUDA,RIGHT_SUBSPACE)(bra, right_subspace_data); - - if (col_idx != -1) { - val += tmp * xarray[col_idx]; - } + val += tmp * xarray[col_idx]; } barray[row_idx] = val; From 970d833c973501fd00865fb0a5235c976f814828 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 2 May 2023 19:13:41 -0700 Subject: [PATCH 27/73] precompute matrix diagonal for shell --- CHANGELOG.md | 2 + benchmarking/benchmark.py | 6 + src/dynamite/_backend/bcuda_impl.cu | 48 +++++--- src/dynamite/_backend/bcuda_impl.h | 2 +- src/dynamite/_backend/bcuda_template_1.cu | 66 +++++++++++ src/dynamite/_backend/bcuda_template_1.h | 12 ++ .../_backend/bcuda_template_1_private.h | 10 ++ ...{bcuda_template.cu => bcuda_template_2.cu} | 25 ++++- .../{bcuda_template.h => bcuda_template_2.h} | 3 +- ...e_private.h => bcuda_template_2_private.h} | 6 +- src/dynamite/_backend/bcuda_template_shared.h | 7 ++ src/dynamite/_backend/bpetsc.pyx | 11 ++ src/dynamite/_backend/bpetsc_impl.c | 28 +++++ src/dynamite/_backend/bpetsc_impl.h | 2 + src/dynamite/_backend/bpetsc_template_1.c | 63 +++++++++++ src/dynamite/_backend/bpetsc_template_1.h | 3 - src/dynamite/_backend/bpetsc_template_2.c | 103 +++++++++++++----- src/dynamite/_backend/makefile | 2 +- src/dynamite/_backend/shell_context.h | 4 + src/dynamite/operators.py | 32 ++++++ tests/integration/test_multiply.py | 25 ++++- tests/unit/test_operators.py | 9 ++ 22 files changed, 404 insertions(+), 65 deletions(-) create mode 100644 src/dynamite/_backend/bcuda_template_1.cu create mode 100644 src/dynamite/_backend/bcuda_template_1.h create mode 100644 src/dynamite/_backend/bcuda_template_1_private.h rename src/dynamite/_backend/{bcuda_template.cu => bcuda_template_2.cu} (96%) rename src/dynamite/_backend/{bcuda_template.h => bcuda_template_2.h} (81%) rename src/dynamite/_backend/{bcuda_template_private.h => bcuda_template_2_private.h} (93%) create mode 100644 src/dynamite/_backend/bcuda_template_shared.h diff --git a/CHANGELOG.md b/CHANGELOG.md index a68044d..91c43fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ - Detailed example scripts (in `examples/scripts`) - `Operator.expectation()`, convenience function to compute the expectation value of the operator with respect to a state - `dynamite.tools.MPI_COMM_WORLD()` which returns PETSc's MPI communicator object + - `Operator.precompute_diagonal` flag allows user to tune whether the matrix diagonal should be precomputed and saved, for shell matrices ### Changed - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` + - shell matrix-vector multiplications are now considerably faster ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index 204fba5..3285e61 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -26,6 +26,8 @@ def parse_args(argv=None): parser.add_argument('--shell', action='store_true', help='Make a shell matrix instead of a regular matrix.') + parser.add_argument('--no-precompute-diagonal', action='store_true', + help='Turn off precomputation of the matrix diagonal for shell matrices.') parser.add_argument('--gpu', action='store_true', help='Run computations on GPU instead of CPU.') @@ -242,6 +244,10 @@ def main(): subspace = log_call(build_subspace, stats)(arg_params, H) if H is not None: H.subspace = subspace + + if arg_params.no_precompute_diagonal: + H.precompute_diagonal = False + Print('H statistics:') Print(' dim:', H.dim[0]) Print(' nnz:', H.nnz) diff --git a/src/dynamite/_backend/bcuda_impl.cu b/src/dynamite/_backend/bcuda_impl.cu index 57f8128..4f61a74 100644 --- a/src/dynamite/_backend/bcuda_impl.cu +++ b/src/dynamite/_backend/bcuda_impl.cu @@ -220,74 +220,90 @@ __device__ static __inline__ void add_imag(PetscScalar *x, PetscReal c) { #define SpinConserve_SP 2 #define Explicit_SP 3 +#define SUBSPACE Full + #include "bcuda_template_1.cu" +#undef SUBSPACE + +#define SUBSPACE Parity + #include "bcuda_template_1.cu" +#undef SUBSPACE + +#define SUBSPACE SpinConserve + #include "bcuda_template_1.cu" +#undef SUBSPACE + +#define SUBSPACE Explicit + #include "bcuda_template_1.cu" +#undef SUBSPACE + #define LEFT_SUBSPACE Full #define RIGHT_SUBSPACE Full - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Parity - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE SpinConserve - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Explicit - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #undef LEFT_SUBSPACE #define LEFT_SUBSPACE Parity #define RIGHT_SUBSPACE Full - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Parity - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE SpinConserve - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Explicit - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #undef LEFT_SUBSPACE #define LEFT_SUBSPACE SpinConserve #define RIGHT_SUBSPACE Full - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Parity - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE SpinConserve - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Explicit - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #undef LEFT_SUBSPACE #define LEFT_SUBSPACE Explicit #define RIGHT_SUBSPACE Full - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Parity - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE SpinConserve - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #define RIGHT_SUBSPACE Explicit - #include "bcuda_template.cu" + #include "bcuda_template_2.cu" #undef RIGHT_SUBSPACE #undef LEFT_SUBSPACE diff --git a/src/dynamite/_backend/bcuda_impl.h b/src/dynamite/_backend/bcuda_impl.h index e0be457..d0d04c1 100644 --- a/src/dynamite/_backend/bcuda_impl.h +++ b/src/dynamite/_backend/bcuda_impl.h @@ -1,5 +1,5 @@ #include -#include "shell_context.h" #include "bsubspace_impl.h" +#include "shell_context.h" diff --git a/src/dynamite/_backend/bcuda_template_1.cu b/src/dynamite/_backend/bcuda_template_1.cu new file mode 100644 index 0000000..28c89d6 --- /dev/null +++ b/src/dynamite/_backend/bcuda_template_1.cu @@ -0,0 +1,66 @@ + +#include "bcuda_template_1_private.h" + +PetscErrorCode C(PrecomputeDiagonal_GPU,SUBSPACE)(Mat A) +{ + PetscInt size; + shell_context *ctx; + PetscCall(MatShellGetContext(A, &ctx)); + + PetscCall(MatGetSize(A, &size, NULL)); + + PetscCallCUDA(cudaMalloc((void **) &(ctx->diag), sizeof(PetscReal)*size)); + + PetscCallCUDA(cudaDeviceSynchronize()); + + C(device_PrecomputeDiagonal,SUBSPACE)<<>>( + size, + ctx->mask_offsets, + ctx->signs, + ctx->real_coeffs, + (C(data,SUBSPACE)*) ctx->right_subspace_data, + ctx->diag); + + PetscCallCUDA(cudaDeviceSynchronize()); + + return 0; +} + +__global__ void C(device_PrecomputeDiagonal,SUBSPACE)( + PetscInt size, + PetscInt* mask_offsets, + PetscInt* signs, + PetscReal* real_coeffs, + C(data,SUBSPACE) *subspace_data, + PetscReal* diag) +{ + + /* the following four lines come from the PETSc cuda source */ + PetscInt entries_per_group = (size - 1) / gridDim.x + 1; + entries_per_group = (entries_per_group == 0) ? 1 : entries_per_group; // for very small vectors, a group should still do some work + PetscInt vec_start_index = blockIdx.x * entries_per_group; + PetscInt vec_stop_index = PetscMin((blockIdx.x + 1) * entries_per_group, size); // don't go beyond vec size + + PetscReal val; + PetscReal sign; + PetscInt state, row_idx, term_idx, this_start; + + this_start = vec_start_index + threadIdx.x; + + for (row_idx=this_start; row_idxnmasks = msc->nmasks; - ctx->nrm = -1; + ctx->gpu = PETSC_TRUE; nterms = msc->mask_offsets[msc->nmasks]; PetscCallCUDA(cudaMalloc((void **) &(ctx->masks), @@ -108,6 +109,10 @@ PetscErrorCode C(MatDestroyCtx_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A) PetscCallCUDA(cudaFree(ctx->signs)); PetscCallCUDA(cudaFree(ctx->real_coeffs)); + if (ctx->diag) { + PetscCallCUDA(cudaFree(ctx->diag)); + } + PetscCall(C(DestroySubspaceData_CUDA,LEFT_SUBSPACE)( (C(data,LEFT_SUBSPACE)*) ctx->left_subspace_data)); PetscCall(C(DestroySubspaceData_CUDA,RIGHT_SUBSPACE)( @@ -146,6 +151,7 @@ PetscErrorCode C(MatMult_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, Vec ctx->nmasks, (C(data,LEFT_SUBSPACE)*) ctx->left_subspace_data, (C(data,RIGHT_SUBSPACE)*) ctx->right_subspace_data, + ctx->diag, xarray, barray); @@ -166,6 +172,7 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( PetscInt nmasks, C(data,LEFT_SUBSPACE) *left_subspace_data, C(data,RIGHT_SUBSPACE) *right_subspace_data, + PetscReal* diag, const PetscScalar* xarray, PetscScalar* barray) { @@ -184,8 +191,16 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( for (row_idx = this_start; row_idx < vec_stop_index; row_idx += blockDim.x) { ket = C(I2S_CUDA,LEFT_SUBSPACE)(row_idx,left_subspace_data); - val = 0; - for (mask_idx = 0; mask_idx < nmasks; ++mask_idx) { + + if (diag) { + val = diag[row_idx] * xarray[row_idx]; + mask_idx = 1; + } else { + val = 0; + mask_idx = 0; + } + + for (; mask_idx #include #include -#include "bcuda_template.h" - -#define GPU_BLOCK_SIZE 1024 -#define GPU_BLOCK_NUM 1024 +#include "bcuda_template_2.h" PetscErrorCode C(BuildContext_CUDA,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( const msc_t *msc, @@ -26,6 +23,7 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( PetscInt nmasks, C(data,LEFT_SUBSPACE) *left_subspace_data, C(data,RIGHT_SUBSPACE) *right_subspace_data, + PetscReal* diag, const PetscScalar* xarray, PetscScalar* barray); diff --git a/src/dynamite/_backend/bcuda_template_shared.h b/src/dynamite/_backend/bcuda_template_shared.h new file mode 100644 index 0000000..f584682 --- /dev/null +++ b/src/dynamite/_backend/bcuda_template_shared.h @@ -0,0 +1,7 @@ +#pragma once + +#define CONCAT_U(a, b) a ## _ ## b +#define C(a, b) CONCAT_U(a, b) + +#define GPU_BLOCK_SIZE 1024 +#define GPU_BLOCK_NUM 1024 diff --git a/src/dynamite/_backend/bpetsc.pyx b/src/dynamite/_backend/bpetsc.pyx index 0a0ba0c..c8b6792 100644 --- a/src/dynamite/_backend/bpetsc.pyx +++ b/src/dynamite/_backend/bpetsc.pyx @@ -49,6 +49,8 @@ cdef extern from "bpetsc_impl.h": PetscInt xparity, PetscMat *A) + int PrecomputeDiagonal(PetscMat A) + int CheckConserves(msc_t *msc, subspaces_t *subspaces, PetscInt xparity, @@ -135,6 +137,15 @@ def build_mat(PetscInt [:] masks, return M +def precompute_diagonal(Mat A): + + cdef int ierr + ierr = PrecomputeDiagonal(A.mat) + + if ierr != 0: + raise Error(ierr) + + def check_conserves(PetscInt [:] masks, PetscInt [:] mask_offsets, PetscInt [:] signs, diff --git a/src/dynamite/_backend/bpetsc_impl.c b/src/dynamite/_backend/bpetsc_impl.c index 38cbbb9..9d327e2 100644 --- a/src/dynamite/_backend/bpetsc_impl.c +++ b/src/dynamite/_backend/bpetsc_impl.c @@ -14,6 +14,10 @@ #define SpinConserve_SP 2 #define Explicit_SP 3 +#define Full_ENUM FULL +#define Parity_ENUM PARITY +#define SpinConserve_ENUM SPIN_CONSERVE +#define Explicit_ENUM EXPLICIT #define SUBSPACE Full #include "bpetsc_template_1.c" @@ -62,6 +66,30 @@ PetscErrorCode ReducedDensityMatrix( return 0; } +#undef __FUNCT__ +#define __FUNCT__ "PrecomputeDiagonal" +PetscErrorCode PrecomputeDiagonal(Mat A){ + shell_context *ctx; + PetscCall(MatShellGetContext(A, &ctx)); + switch (ctx->right_subspace_type) { + case FULL: + PetscCall(PrecomputeDiagonal_Full(A)); + break; + case PARITY: + PetscCall(PrecomputeDiagonal_Parity(A)); + break; + case SPIN_CONSERVE: + PetscCall(PrecomputeDiagonal_SpinConserve(A)); + break; + case EXPLICIT: + PetscCall(PrecomputeDiagonal_Explicit(A)); + break; + default: // shouldn't happen, but give ierr some (nonzero) value for consistency + return 1; + } + return 0; +} + #define LEFT_SUBSPACE Full #define RIGHT_SUBSPACE Full #include "bpetsc_template_2.c" diff --git a/src/dynamite/_backend/bpetsc_impl.h b/src/dynamite/_backend/bpetsc_impl.h index 47a6a0a..6c9a753 100644 --- a/src/dynamite/_backend/bpetsc_impl.h +++ b/src/dynamite/_backend/bpetsc_impl.h @@ -43,6 +43,8 @@ PetscErrorCode BuildMat(const msc_t *msc, subspaces_t *subspaces, shell_impl she PetscErrorCode CheckConserves(const msc_t *msc, subspaces_t *subspaces, int xparity, PetscBool *result); +PetscErrorCode PrecomputeDiagonal(Mat A); + PetscErrorCode BuildContext(const msc_t *msc, const void* left_subspace_data, const void* right_subspace_data, diff --git a/src/dynamite/_backend/bpetsc_template_1.c b/src/dynamite/_backend/bpetsc_template_1.c index a6512a4..e65c636 100644 --- a/src/dynamite/_backend/bpetsc_template_1.c +++ b/src/dynamite/_backend/bpetsc_template_1.c @@ -4,6 +4,9 @@ */ #include "bpetsc_template_1.h" +#if PETSC_HAVE_CUDA +#include "bcuda_template_1.h" +#endif /* * this function is actually the same for all subspaces but @@ -160,3 +163,63 @@ PetscErrorCode C(rdm,SUBSPACE)( return 0; } + +#undef __FUNCT__ +#define __FUNCT__ "PrecomputeDiagonal_CPU" +PetscErrorCode C(PrecomputeDiagonal_CPU,SUBSPACE)(Mat A){ + PetscInt row_start, row_end, row_idx, term_idx; + PetscInt sign, state; + PetscReal value; + + shell_context *ctx; + PetscCall(MatShellGetContext(A, &ctx)); + + if (ctx->masks[0] != 0) { + /* there is no diagonal! leave diag as NULL */ + return 0; + } + + PetscCall(MatGetOwnershipRange(A, &row_start, &row_end)); + + PetscCall(PetscMalloc1(row_end-row_start, &(ctx->diag))); + + for (row_idx=row_start; row_idxright_subspace_data); + } else { + state = C(NextState,SUBSPACE)(state, row_idx, ctx->right_subspace_data); + } + + value = 0; + for (term_idx=0; term_idxmask_offsets[1]; ++term_idx) { + sign = 1 - 2*(builtin_parity(state & ctx->signs[term_idx])); + value += sign * ctx->real_coeffs[term_idx]; + } + ctx->diag[row_idx-row_start] = value; + } + + return 0; +} + +#undef __FUNCT__ +#define __FUNCT__ "PrecomputeDiagonal" +PetscErrorCode C(PrecomputeDiagonal,SUBSPACE)(Mat A) +{ + PetscErrorCode ierr; + shell_context *ctx; + PetscCall(MatShellGetContext(A, &ctx)); + + if (!(ctx->gpu)) { + ierr = C(PrecomputeDiagonal_CPU,SUBSPACE)(A); + } +#if PETSC_HAVE_CUDA + else { + ierr = C(PrecomputeDiagonal_GPU,SUBSPACE)(A); + } +#else + else { + SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_UNKNOWN_TYPE, "GPU not enabled for this build"); + } +#endif + return ierr; +} diff --git a/src/dynamite/_backend/bpetsc_template_1.h b/src/dynamite/_backend/bpetsc_template_1.h index eeb49f2..3165765 100644 --- a/src/dynamite/_backend/bpetsc_template_1.h +++ b/src/dynamite/_backend/bpetsc_template_1.h @@ -2,9 +2,6 @@ #define CONCAT_U(a, b) a ## _ ## b #define C(a, b) CONCAT_U(a, b) -/* - * This function is called to build any matrix. - */ PetscErrorCode C(rdm,SUBSPACE)( Vec vec, const C(data,SUBSPACE)* sub_data_p, diff --git a/src/dynamite/_backend/bpetsc_template_2.c b/src/dynamite/_backend/bpetsc_template_2.c index 724b176..33073f5 100644 --- a/src/dynamite/_backend/bpetsc_template_2.c +++ b/src/dynamite/_backend/bpetsc_template_2.c @@ -6,7 +6,7 @@ #include "bpetsc_template_2.h" #if PETSC_HAVE_CUDA -#include "bcuda_template.h" +#include "bcuda_template_2.h" #endif #undef __FUNCT__ @@ -24,18 +24,30 @@ PetscErrorCode C(BuildMat,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( ierr = C(BuildPetsc,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( msc, left_subspace_data, right_subspace_data, xparity, A); } - else if (shell == CPU_SHELL) { - ierr = C(BuildCPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( - msc, left_subspace_data, right_subspace_data, xparity, A); - } + else { + if (shell == CPU_SHELL) { + ierr = C(BuildCPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( + msc, left_subspace_data, right_subspace_data, xparity, A); + } #if PETSC_HAVE_CUDA - else if (shell == GPU_SHELL) { - ierr = C(BuildGPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( - msc, left_subspace_data, right_subspace_data, xparity, A); - } + else if (shell == GPU_SHELL) { + ierr = C(BuildGPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( + msc, left_subspace_data, right_subspace_data, xparity, A); + } #endif - else { - SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_UNKNOWN_TYPE, "Invalid shell implementation type."); + else { + SETERRQ(PETSC_COMM_SELF, PETSC_ERR_ARG_UNKNOWN_TYPE, "Invalid shell implementation type."); + } + + /* set some data that is shared by CPU and GPU shell implementations */ + shell_context *ctx; + PetscCall(MatShellGetContext(*A, &ctx)); + + ctx->nmasks = msc->nmasks; + ctx->nrm = -1; + ctx->diag = NULL; // diag is allocated later, if filled + ctx->left_subspace_type = C(LEFT_SUBSPACE,ENUM); + ctx->right_subspace_type = C(RIGHT_SUBSPACE,ENUM); } return ierr; } @@ -226,6 +238,7 @@ PetscErrorCode C(BuildCPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( msc, left_subspace_data, right_subspace_data, &ctx)); PetscCall(MatCreateShell(PETSC_COMM_WORLD, m, n, M, N, ctx, A)); + PetscCall(MatShellSetOperation(*A, MATOP_MULT, (void(*)(void))C(MatMult_CPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE)))); PetscCall(MatShellSetOperation(*A, MATOP_NORM, @@ -247,6 +260,8 @@ PetscErrorCode C(BuildContext_CPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( const C(data,RIGHT_SUBSPACE)* right_subspace_data, shell_context **ctx_p) { + /* NOTE: some data shared by GPU and CPU implementations is set in BuildMat */ + shell_context *ctx; PetscInt nterms, i; PetscReal real_part; @@ -254,8 +269,7 @@ PetscErrorCode C(BuildContext_CPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( PetscCall(PetscMalloc1(1, ctx_p)); ctx = (*ctx_p); - ctx->nmasks = msc->nmasks; - ctx->nrm = -1; + ctx->gpu = PETSC_FALSE; nterms = msc->mask_offsets[msc->nmasks]; /* we need to keep track of this stuff on our own. the numpy array might get garbage collected */ @@ -298,6 +312,10 @@ PetscErrorCode C(MatDestroyCtx_CPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A) PetscCall(PetscFree(ctx->signs)); PetscCall(PetscFree(ctx->real_coeffs)); + if (ctx->diag) { + PetscCall(PetscFree(ctx->diag)); + } + PetscCall(C(DestroySubspaceData,LEFT_SUBSPACE)(ctx->left_subspace_data)); PetscCall(C(DestroySubspaceData,RIGHT_SUBSPACE)(ctx->right_subspace_data)); @@ -360,7 +378,16 @@ PetscErrorCode C(MatMult_CPU_General,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec ket = C(NextState,LEFT_SUBSPACE)(ket, row_idx, ctx->left_subspace_data); } - for (mask_idx = 0; mask_idx < ctx->nmasks; mask_idx++) { + // handle the diagonal separately first, if we have it cached + if (ctx->diag) { + b_array[row_idx-row_start] += ctx->diag[row_idx-row_start] * local_x_array[row_idx-row_start]; + mask_idx = 1; // skip it below! + } else { + mask_idx = 0; + } + + for (; mask_idx < ctx->nmasks; mask_idx++) { + bra = ket ^ ctx->masks[mask_idx]; col_idx = C(S2I,RIGHT_SUBSPACE)(bra, ctx->right_subspace_data); @@ -404,22 +431,30 @@ PetscErrorCode C(MatMult_CPU_General,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec } for (mask_idx=0; mask_idxnmasks; mask_idx++) { - bra = ket ^ ctx->masks[mask_idx]; - - row_idx = C(S2I,LEFT_SUBSPACE)(bra, ctx->left_subspace_data); - if (row_idx == -1) continue; + // handle the diagonal separately first, if we have it cached + if (mask_idx == 0 && ctx->diag) { + row_idx = col_idx; + value = ctx->diag[col_idx-col_start]; + } else { + bra = ket ^ ctx->masks[mask_idx]; + + row_idx = C(S2I,LEFT_SUBSPACE)(bra, ctx->left_subspace_data); + + if (row_idx == -1) continue; + + /* sum all terms for this matrix element */ + value = 0; + for (term_idx=ctx->mask_offsets[mask_idx]; term_idxmask_offsets[mask_idx+1]; ++term_idx) { + sign = 1 - 2*(builtin_parity(ket&ctx->signs[term_idx])); + if (TERM_REAL(ctx->masks[mask_idx], ctx->signs[term_idx])) { + value += sign * ctx->real_coeffs[term_idx]; + } else { + value += I * sign * ctx->real_coeffs[term_idx]; + } + } + } - /* sum all terms for this matrix element */ - value = 0; - for (term_idx=ctx->mask_offsets[mask_idx]; term_idxmask_offsets[mask_idx+1]; ++term_idx) { - sign = 1 - 2*(builtin_parity(ket&ctx->signs[term_idx])); - if (TERM_REAL(ctx->masks[mask_idx], ctx->signs[term_idx])) { - value += sign * ctx->real_coeffs[term_idx]; - } else { - value += I * sign * ctx->real_coeffs[term_idx]; - } - } if (cache_idx >= VECSET_CACHE_SIZE) { SETERRQ(MPI_COMM_SELF, PETSC_ERR_MEMC, "cache out of bounds, value %" PetscInt_FMT, cache_idx); } @@ -772,7 +807,17 @@ PetscErrorCode C(MatMult_CPU_Fast,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, row_idx[cache_idx] = block_start_idx+cache_idx; } - for (mask_idx = mask_starts[proc_idx]; mask_idx < mask_starts[proc_idx+1]; ++mask_idx) { + // handle the diagonal separately first, if we have it cached + if (proc_idx==0 && ctx->diag) { + for (cache_idx=0; cache_idx < VECSET_CACHE_SIZE; ++cache_idx) { + values[cache_idx] = ctx->diag[(block_start_idx-x_start)+cache_idx] * x_array[(block_start_idx-x_start)+cache_idx]; + } + mask_idx = 1; // skip it below! + } else { + mask_idx = mask_starts[proc_idx]; + } + + for (; mask_idx < mask_starts[proc_idx+1]; ++mask_idx) { #if (C(LEFT_SUBSPACE,SP) == Parity_SP) /* skip terms that don't preserve parity */ diff --git a/src/dynamite/_backend/makefile b/src/dynamite/_backend/makefile index 453dfbb..1761bbc 100644 --- a/src/dynamite/_backend/makefile +++ b/src/dynamite/_backend/makefile @@ -3,4 +3,4 @@ include ${SLEPC_DIR}/lib/slepc/conf/slepc_common bpetsc_impl.o: bpetsc_impl.c bpetsc_impl.h bsubspace_impl.h shell_context.h bpetsc_template_1.h bpetsc_template_1.c bpetsc_template_2.h bpetsc_template_2.c -bcuda_impl.o: bcuda_impl.cu bcuda_impl.h bsubspace_impl.h shell_context.h bcuda_template.h bcuda_template_private.h bcuda_template.cu +bcuda_impl.o: bcuda_impl.cu bcuda_impl.h bsubspace_impl.h shell_context.h bcuda_template_2.h bcuda_template_2_private.h bcuda_template_2.cu bcuda_template_1.h bcuda_template_1_private.h bcuda_template_1.cu diff --git a/src/dynamite/_backend/shell_context.h b/src/dynamite/_backend/shell_context.h index a4feaaa..4ef6d8e 100644 --- a/src/dynamite/_backend/shell_context.h +++ b/src/dynamite/_backend/shell_context.h @@ -15,7 +15,11 @@ typedef struct _shell_context { PetscInt* mask_offsets; PetscInt* signs; PetscReal* real_coeffs; // we store only the real or complex part -- whichever is nonzero + PetscReal *diag; + subspace_type left_subspace_type; void *left_subspace_data; + subspace_type right_subspace_type; void *right_subspace_data; PetscReal nrm; + PetscBool gpu; } shell_context; diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index 20c3762..efecfd6 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -30,6 +30,7 @@ def __init__(self): self._msc = None self._is_reduced = False self._shell = config.shell + self._precompute_diagonal = True self._allow_projection = False if config.subspace is not None: @@ -273,6 +274,33 @@ def shell(self,value): self.destroy_mat() self._shell = value + @property + def precompute_diagonal(self): + """ + Whether shell matrices should precompute and store the matrix diagonal. + Usually should only be turned off if a matrix will be "single-use" (destroyed + after a single multiplication). + + .. note:: + Changing this value after the matrix has been built will invoke a call + to :meth:`Operator.destroy_mat`. + """ + if not self.shell: + raise ValueError("precompute_diagonal only applies when shell=True") + return self._precompute_diagonal + + @precompute_diagonal.setter + def precompute_diagonal(self, value): + value = bool(value) + + if not self.shell: + raise ValueError("precompute_diagonal only applies when shell=True") + + if value != self.precompute_diagonal: + self.destroy_mat() + + self._precompute_diagonal = value + @property def left_subspace(self): """ @@ -629,6 +657,10 @@ def build_mat(self, subspaces=None): gpu=config.gpu, ) + if (self.shell and self.precompute_diagonal + and subspaces[0] == subspaces[1] and masks[0] == 0): + bpetsc.precompute_diagonal(mat) + self._mats[subspaces] = mat @classmethod diff --git a/tests/integration/test_multiply.py b/tests/integration/test_multiply.py index 429d2fd..88c607b 100644 --- a/tests/integration/test_multiply.py +++ b/tests/integration/test_multiply.py @@ -61,16 +61,23 @@ def test_spin_flip(self): @generate_hamiltonian_tests class FullHamiltonians(dtr.DynamiteTestCase): def check_hamiltonian(self, H_name): + self._check_hamiltonian(H_name) + + with self.subTest(no_diag=True): + if config.shell: + self._check_hamiltonian(H_name, no_diag=True) + + def _check_hamiltonian(self, H_name, no_diag=False): if H_name == 'syk': self.skip_on_flag('small_only') if H_name == 'long_range': self.skip_on_flag('medium_only') H = getattr(hamiltonians, H_name)() bra, ket = H.create_states() - - #ket.set_product(0) ket.set_random(seed = 0) - #ket.vec.set(1) + + if no_diag: + H.precompute_diagonal = False H.dot(ket, bra) self.assertLess(1E-3, bra.vec.norm(), msg = 'petsc vec norm incorrect') @@ -117,6 +124,15 @@ def compare_to_full(self, H, x_sub, x_full, check_subspace): check_subspace : dynamite.subspace.Subspace The subspace to multiply under. ''' + self._compare_to_full(H, x_sub, x_full, check_subspace) + + if config.shell: + with self.subTest(no_diag=True): + H.precompute_diagonal = False + self._compare_to_full(H, x_sub, x_full, check_subspace) + H.precompute_diagonal = True + + def _compare_to_full(self, H, x_sub, x_full, check_subspace): extra_conversion = isinstance(check_subspace, XParity) # compare all possible combinations of going to and from the full space @@ -201,7 +217,8 @@ def check_s2s(self, H, x_sub, check_subspace, correct): check multiplication from subspace to subspace ''' H.add_subspace(check_subspace) - result = H.dot(x_sub) + result = State(subspace=check_subspace) + H.dot(x_sub, result=result) eps = H.nnz*np.finfo(msc_dtype[2]).eps self.check_vec_equal(correct, result, eps=eps) diff --git a/tests/unit/test_operators.py b/tests/unit/test_operators.py index 1e85276..d876bca 100644 --- a/tests/unit/test_operators.py +++ b/tests/unit/test_operators.py @@ -883,6 +883,15 @@ def test_brackets(self): with self.assertRaises(ValueError): o._string_rep.brackets = '<>' + def test_precompute_diagonal_fail(self): + # this should only work when shell=True + o = sigmaz() + with self.assertRaises(ValueError): + o.precompute_diagonal + + with self.assertRaises(ValueError): + o.precompute_diagonal = True + class MSC(ut.TestCase): ''' From b1bfbc226848d75c388ab18aeb6b0c819fa11c99 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 2 May 2023 19:15:55 -0700 Subject: [PATCH 28/73] fix test failing due to floating point errors --- tests/integration/test_operators.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_operators.py b/tests/integration/test_operators.py index 1a587ee..afc98a0 100644 --- a/tests/integration/test_operators.py +++ b/tests/integration/test_operators.py @@ -273,16 +273,18 @@ def test_simple(self): state = State(state='random') correct = self.expectation_correct(H, state) - self.assertEqual( - H.expectation(state), - correct - ) + with self.subTest(with_tmp=False): + self.assertLess( + abs(H.expectation(state) - correct), + 1E-15 + ) - tmp = State() - self.assertEqual( - H.expectation(state, tmp_state=tmp), - correct - ) + with self.subTest(with_tmp=True): + tmp = State() + self.assertLess( + abs(H.expectation(state, tmp_state=tmp) - correct), + 1E-15 + ) def test_uninitialized_fail(self): with self.assertRaises(UninitializedError): From cb782e40b23c987d5ff33ced19fc308c552a8620 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 3 May 2023 12:30:53 -0700 Subject: [PATCH 29/73] fix inefficient sign flip in CUDA shell --- src/dynamite/_backend/bcuda_template_2.cu | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/dynamite/_backend/bcuda_template_2.cu b/src/dynamite/_backend/bcuda_template_2.cu index 0d3c201..9cf212b 100644 --- a/src/dynamite/_backend/bcuda_template_2.cu +++ b/src/dynamite/_backend/bcuda_template_2.cu @@ -216,12 +216,18 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( #else sign = __popc(bra & signs[term_idx])&1; #endif - sign = 1 - 2*sign; if TERM_REAL_CUDA(masks[mask_idx], signs[term_idx]) { - add_real(&tmp, sign * real_coeffs[term_idx]); - } - else { - add_imag(&tmp, sign * real_coeffs[term_idx]); + if (sign) { + add_real(&tmp, -real_coeffs[term_idx]); + } else { + add_real(&tmp, real_coeffs[term_idx]); + } + } else { + if (sign) { + add_imag(&tmp, -real_coeffs[term_idx]); + } else { + add_imag(&tmp, real_coeffs[term_idx]); + } } } val += tmp * xarray[col_idx]; From 28519b111171790802607d3662d961231abb2f02 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 3 May 2023 21:16:48 -0700 Subject: [PATCH 30/73] fix constant factors in benchmark hamiltonians --- benchmarking/benchmark.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index 3285e61..c0b1a90 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -3,6 +3,7 @@ from random import uniform,seed from timeit import default_timer from itertools import combinations +import numpy as np from dynamite import config from dynamite.states import State @@ -120,19 +121,19 @@ def build_hamiltonian(params): if params.H == 'MBL': # dipolar interaction - rtn = index_sum(op_sum(s(0)*s(1) for s in (sigmax, sigmay, sigmaz))) + rtn = index_sum(op_sum(0.25*s(0)*s(1) for s in (sigmax, sigmay, sigmaz))) # quenched disorder in z direction seed(0) for i in range(params.L): - rtn += uniform(-1, 1) * sigmaz(i) + rtn += uniform(-3, 3) * 0.5 * sigmaz(i) elif params.H == 'long_range': # long-range ZZ interaction - rtn = op_sum(index_sum(sigmaz(0)*sigmaz(i)) for i in range(1, params.L)) + rtn = op_sum(index_sum(0.25*sigmaz(0)*sigmaz(i)) for i in range(1, params.L)) # nearest neighbor XX - rtn += 0.5 * index_sum(sigmax(0)*sigmax(1)) + rtn += 0.5 * index_sum(0.25*sigmax(0)*sigmax(1)) # some other fields - rtn += sum(0.1*index_sum(s()) for s in [sigmax, sigmay, sigmaz]) + rtn += sum(0.05*index_sum(s()) for s in [sigmax, sigmay, sigmaz]) elif params.H == 'SYK': seed(0) @@ -147,15 +148,16 @@ def gen_products(L): yield p rtn = op_sum(gen_products(params.L)) + rtn.scale(np.sqrt(6/(params.L*2)**3)) elif params.H == 'ising': - rtn = index_sum(sigmaz(0)*sigmaz(1)) + 0.2*index_sum(sigmax()) + rtn = index_sum(0.25*sigmaz(0)*sigmaz(1)) + 0.1*index_sum(sigmax()) elif params.H == 'XX': - rtn = index_sum(sigmax(0)*sigmax(1)) + rtn = index_sum(0.25*sigmax(0)*sigmax(1)) elif params.H == 'heisenberg': - rtn = index_sum(sigmax(0)*sigmax(1) + sigmay(0)*sigmay(1) + sigmaz(0)*sigmaz(1)) + rtn = index_sum(op_sum(0.25*s(0)*s(1) for s in (sigmax, sigmay, sigmaz))) else: raise ValueError('Unrecognized Hamiltonian.') From eefc10159e386bf8da31cead33add3e1ffbb6b0a Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 5 May 2023 12:03:20 -0700 Subject: [PATCH 31/73] normalize evolution time by mat norm in benchmarks and tests --- benchmarking/benchmark.py | 32 +++++++++++++++++++++++++------- tests/integration/test_evolve.py | 11 +++++++---- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index c0b1a90..e27574c 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -54,8 +54,12 @@ def parse_args(argv=None): parser.add_argument('--evolve', action='store_true', help='Request that the Hamiltonian evolves a state.') - parser.add_argument('-t', type=float, default=1.0, + parser.add_argument('-t', type=float, default=50.0, help='The time to evolve for.') + parser.add_argument('--no_normalize_t', action='store_true', + help='Turn off the default behavior of dividing the evolve time by the ' + 'matrix norm, which should yield a fairer comparison across models' + ' and system sizes.') parser.add_argument('--mult', action='store_true', help='Simply multiply the Hamiltonian by a vector.') @@ -79,9 +83,16 @@ def parse_args(argv=None): 'RDM computation. By default, the first half are kept.') parser.add_argument('--check-conserves', action='store_true', - help='Check whether the given subspace is conserved by the matrix.') + help='Benchmark the check for whether the given subspace is conserved by ' + 'the matrix.') - return parser.parse_args(argv) + args = parser.parse_args(argv) + + # we need the norm anyway for this; might as well benchmark it + if args.evolve and not args.no_normalize_t: + args.norm = True + + return args def build_subspace(params, hamiltonian=None): space = params.which_space @@ -162,18 +173,25 @@ def gen_products(L): else: raise ValueError('Unrecognized Hamiltonian.') + # conservation check can take a long time; we benchmark it separately + # TODO: speed up CheckConserves and remove this + rtn.allow_projection = True + return rtn def compute_norm(hamiltonian): - config._initialize() - from petsc4py.PETSc import NormType - return hamiltonian.get_mat().norm(NormType.INFINITY) + return hamiltonian.infinity_norm() def do_eigsolve(params, hamiltonian): hamiltonian.eigsolve(nev=params.nev,target=params.target) def do_evolve(params, hamiltonian, state, result): - hamiltonian.evolve(state, t=params.t, result=result) + # norm should be precomputed by now so the following shouldn't affect + # the measured cost of time evolution + t = params.t + if not params.no_normalize_t: + t /= hamiltonian.infinity_norm() + hamiltonian.evolve(state, t=t, result=result) def do_mult(params, hamiltonian, state, result): for _ in range(params.mult_count): diff --git a/tests/integration/test_evolve.py b/tests/integration/test_evolve.py index 8bbd4d7..bab66ef 100644 --- a/tests/integration/test_evolve.py +++ b/tests/integration/test_evolve.py @@ -39,6 +39,9 @@ def evolve_check(self, H, t, **kwargs): H_np = H.to_numpy() ket_np = ket.to_numpy() + # make the evolutions not affected by normalization of H + t /= H.infinity_norm() + H.evolve(ket, t=t, result=bra, **kwargs) if t.imag == 0: @@ -74,7 +77,7 @@ def evolve_all(self, t, skip=None, **kwargs): # do without complex numbers evolve_types += ['real'] - if t < 20: # imaginary doesn't converge for huge t + if t < 200: # imaginary doesn't converge for huge t evolve_types += ['imaginary'] for evolve_type in evolve_types: @@ -100,7 +103,7 @@ def test_short(self): else: skip = {} - self.evolve_all(0.1, skip=skip) + self.evolve_all(1.0, skip=skip) def test_long(self): # skip all hamiltonians for this test on medium-only @@ -113,7 +116,7 @@ def test_long(self): skip = {} # otherwise just skip syk - self.evolve_all(50.0, skip=skip, max_its=750) + self.evolve_all(500.0, skip=skip) @ut.skipIf(not complex_enabled(), 'complex numbers not enabled') class Subspaces(EvolveChecker): @@ -155,7 +158,7 @@ def test_all_subspaces(self): with self.subTest(subspace=subspace): H.add_subspace(subspace) H.allow_projection = True - self.evolve_check(H, 0.1) + self.evolve_check(H, 1.5) def test_parity_exceptions(self): H = identity() From 3b41c15e7d9abd4f8ef000586a6e7f95d9a73ef5 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 5 May 2023 12:23:21 -0700 Subject: [PATCH 32/73] include total time in benchmarks --- benchmarking/benchmark.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index e27574c..d36a917 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -232,6 +232,8 @@ def rtn(*args, **kwargs): return rtn def main(): + main_start = default_timer() + arg_params = parse_args() slepc_args = arg_params.slepc_args.split(' ') config.initialize(slepc_args, gpu=arg_params.gpu) @@ -314,6 +316,8 @@ def main(): stats['Gb_memory'] = get_max_memory_usage() + stats['total_time'] = default_timer() - main_start + Print('---RESULTS---') for k,v in stats.items(): Print('{0}, {1:0.4f}'.format(k, v)) From db55e427f457a17f724b1b8213abdfc28f142f96 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 5 May 2023 16:42:44 -0700 Subject: [PATCH 33/73] sum memory across all ranks in benchmark --- benchmarking/benchmark.py | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index d36a917..3f91a76 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -11,7 +11,7 @@ from dynamite.operators import op_sum, op_product, index_sum from dynamite.extras import majorana from dynamite.subspaces import Full, Parity, SpinConserve, Auto, XParity -from dynamite.tools import track_memory, get_max_memory_usage +from dynamite.tools import track_memory, get_max_memory_usage, MPI_COMM_WORLD, mpi_print from dynamite.computations import reduced_density_matrix @@ -35,7 +35,7 @@ def parse_args(argv=None): parser.add_argument('--slepc_args', type=str, default='', help='Arguments to pass to SLEPc.') parser.add_argument('--track_memory', action='store_true', - help='Whether to compute max memory usage') + help='Whether to compute max memory usage (summed across all ranks)') parser.add_argument('--subspace', choices=['full', 'parity', 'spinconserve', @@ -205,10 +205,6 @@ def do_check_conserves(hamiltonian): # this decorator keeps track of and times function calls def log_call(function, stat_dict, alt_name=None): - config._initialize() - from petsc4py.PETSc import Sys - Print = Sys.Print - if alt_name is None: fn_name = function.__name__ else: @@ -216,14 +212,14 @@ def log_call(function, stat_dict, alt_name=None): def rtn(*args, **kwargs): if __debug__: - Print('beginning', fn_name) + mpi_print('beginning', fn_name) tick = default_timer() rtn_val = function(*args, **kwargs) tock = default_timer() if __debug__: - Print('completed', fn_name) + mpi_print('completed', fn_name) stat_dict[fn_name] = tock-tick @@ -240,13 +236,10 @@ def main(): config.L = arg_params.L config.shell = arg_params.shell - from petsc4py.PETSc import Sys - Print = Sys.Print - if not __debug__: - Print('---ARGUMENTS---') + mpi_print('---ARGUMENTS---') for k,v in vars(arg_params).items(): - Print(str(k)+','+str(v)) + mpi_print(str(k)+','+str(v)) if arg_params.track_memory: track_memory() @@ -270,11 +263,11 @@ def main(): if arg_params.no_precompute_diagonal: H.precompute_diagonal = False - Print('H statistics:') - Print(' dim:', H.dim[0]) - Print(' nnz:', H.nnz) - Print(' density:', H.density) - Print(' nterms:', H.nterms) + mpi_print('H statistics:') + mpi_print(' dim:', H.dim[0]) + mpi_print(' nnz:', H.nnz) + mpi_print(' density:', H.density) + mpi_print(' nterms:', H.nterms) log_call(H.build_mat, stats)() # build some states to use in the computations @@ -309,18 +302,25 @@ def main(): if arg_params.track_memory: # trigger memory measurement + # TODO: is this still required? if H is not None: H.destroy_mat() elif in_state is not None: in_state.vec.destroy() - stats['Gb_memory'] = get_max_memory_usage() + # sum the memory usage from all ranks + local_mem = get_max_memory_usage() + if MPI_COMM_WORLD().size == 1: + stats['Gb_memory'] = local_mem + else: + comm = MPI_COMM_WORLD().tompi4py() + stats['Gb_memory'] = comm.allreduce(local_mem) stats['total_time'] = default_timer() - main_start - Print('---RESULTS---') + mpi_print('---RESULTS---') for k,v in stats.items(): - Print('{0}, {1:0.4f}'.format(k, v)) + mpi_print('{0}, {1:0.4f}'.format(k, v)) if __name__ == '__main__': main() From 03364dea2848cc1004f19b195c994caa3681dbdd Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 8 May 2023 13:11:37 -0700 Subject: [PATCH 34/73] output avg mult time from benchmarks --- benchmarking/benchmark.py | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index 3f91a76..5421d15 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -290,6 +290,7 @@ def main(): if arg_params.mult: log_call(do_mult, stats)(arg_params, H, in_state, out_state) + stats['avg_mult_time'] = stats['do_mult'] / arg_params.mult_count if arg_params.rdm: keep_idxs = arg_params.keep From 310b5ae821488ea16850dc2faaf88165cbeabdf6 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 1 Jun 2023 23:00:55 -0700 Subject: [PATCH 35/73] allow minor version number to differ for petsc4py/slepc4py --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2db3f7a..845531f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ "numpy", "scipy", "threadpoolctl", - "petsc4py == 3.19.0", - "slepc4py == 3.19.0", + "petsc4py ~= 3.19.0", + "slepc4py ~= 3.19.0", ] [build-system] From e4095f46f4ded561858cc8292d7c56676287251d Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 17 Aug 2023 07:33:25 -0700 Subject: [PATCH 36/73] skip extra header rows due to restarting in MBL example --- examples/scripts/MBL/plot_results.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/scripts/MBL/plot_results.py b/examples/scripts/MBL/plot_results.py index b06ba73..40f156c 100644 --- a/examples/scripts/MBL/plot_results.py +++ b/examples/scripts/MBL/plot_results.py @@ -9,6 +9,9 @@ def read_data(): reader = DictReader(fileinput.input()) rtn = defaultdict(lambda: defaultdict(list)) for row in reader: + # encountered another header row + if row['h'] == 'h': + continue # average over data points key = (float(row['h']), float(row['energy_point'])) for k in ('entropy', 'ratio'): From 19bdaa6ed14c314e4eba6654a33e0619c7ce4677 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 17 Aug 2023 07:34:53 -0700 Subject: [PATCH 37/73] add brief pause between integration tests --- tests/integration/run_all_tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/run_all_tests.py b/tests/integration/run_all_tests.py index fea29be..157dd34 100644 --- a/tests/integration/run_all_tests.py +++ b/tests/integration/run_all_tests.py @@ -6,6 +6,7 @@ from subprocess import run, PIPE, TimeoutExpired from glob import glob import argparse +from time import sleep def parse_command_line(cmd_argv=None): parser = argparse.ArgumentParser(description='Run all tests for dynamite.') @@ -140,6 +141,7 @@ def main(): for nproc in params.nprocs: run_test(params.mpiexec, nproc, test_name, options+const_options, timeout=params.timeout) + sleep(0.1) if __name__ == '__main__': main() From 293ebd56c26533ac3a5856e9a715f83aab85f07b Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 18 Sep 2023 13:43:00 -1000 Subject: [PATCH 38/73] add gong paper --- docs/pubs.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/pubs.md b/docs/pubs.md index 6a8c85b..d2876f2 100644 --- a/docs/pubs.md +++ b/docs/pubs.md @@ -10,3 +10,4 @@ - Schuster *et al.*, “Learning quantum systems via out-of-time-order correlators.” [arXiv, Aug. 03, 2022.](http://arxiv.org/abs/2208.02254) - Schuster *et al.*, “Operator Growth in Open Quantum Systems.” [arXiv, Aug. 25, 2022.](https://doi.org/10.48550/arXiv.2208.12272) - Bornet *et al.*, “Scalable spin squeezing in a dipolar Rydberg atom array.” [arXiv, Mar. 14 2023.](https://doi.org/10.48550/arXiv.2303.08053) + - Gong *et al.*, “Coherent dynamics of strongly interacting electronic spin defects in hexagonal boron nitride.” [Nat. Commun. 14, 3299 (2023).](https://doi.org/10.1038/s41467-023-39115-y) From 197eec39d1d8dc62df6582c3f29b1e9bbb76a3dc Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 10:55:35 -1000 Subject: [PATCH 39/73] fixes for build system changes --- docker/Dockerfile | 14 +++++----- docs/install.rst | 9 ++++--- docs/rtd_requirements.txt | 42 ++++++++++++++--------------- pyproject.toml | 4 +-- src/dynamite/_backend/bsubspace.pyx | 2 +- src/dynamite/subspaces.py | 24 ++++++++--------- 6 files changed, 47 insertions(+), 48 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 358c491..e0171c6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,9 @@ ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' -ARG PETSC_VERSION=3.19.0 -ARG SLEPC_VERSION=3.19.0 -ARG CUDA_VERSION=11.8.0 +ARG PETSC_VERSION=3.20.1 +ARG SLEPC_VERSION=3.20.0 +ARG CUDA_VERSION=12.2.2 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 ARG SCALAR_TYPE=complex @@ -20,7 +20,6 @@ RUN apt-get update && \ cmake \ mpi-default-dev \ libopenblas-dev \ - libmumps-dev \ git \ && \ apt-get clean && \ @@ -31,7 +30,7 @@ ENV PETSC_ARCH=complex-$BUILD_TYPE ARG SCALAR_TYPE ARG PETSC_CONFIG_FLAGS -ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --download-mumps=0 --with-mumps=1 --with-scalar-type=$SCALAR_TYPE" +ENV PETSC_CONFIG_FLAGS="$PETSC_CONFIG_FLAGS --download-mumps=1 --with-scalar-type=$SCALAR_TYPE" FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${CUDA_UBUNTU_VERSION} AS base-gpu @@ -75,7 +74,7 @@ USER dnm # activate venv ENV VIRTUAL_ENV=/venv ENV PATH="$VIRTUAL_ENV/bin:$PATH" -RUN pip3 install --no-cache-dir --upgrade pip wheel +RUN pip3 install --no-cache-dir --upgrade pip~=23.0.1 wheel from build as petsc @@ -86,7 +85,7 @@ ARG BUILD_TYPE # install PETSc USER root WORKDIR /opt -RUN curl --no-progress-meter https://ftp.mcs.anl.gov/pub/petsc/release-snapshots/petsc-$PETSC_VERSION.tar.gz | tar xzf - +RUN curl --no-progress-meter https://web.cels.anl.gov/projects/petsc/download/release-snapshots/petsc-$PETSC_VERSION.tar.gz | tar xzf - RUN mv petsc-$PETSC_VERSION petsc RUN chown -R dnm:dnm petsc USER dnm @@ -148,7 +147,6 @@ ONBUILD RUN apt-get update && \ apt-get install -y --no-install-recommends \ mpi-default-bin \ libopenblas0 \ - libmumps-5.3 \ && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* diff --git a/docs/install.rst b/docs/install.rst index c92f269..a78fe6a 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -72,6 +72,11 @@ paste that, and run it. It should look like: Building dynamite ----------------- +.. note:: + Due to `an issue `_ in PETSc/SLEPc, ``dynamite`` + will only build successfully with ``pip < 23.1``. To ensure a successful build we recommend + running ``pip install pip~=23.0.1`` before running the below commands. + Make sure ``PETSC_DIR`` and ``PETSC_ARCH`` environment variables are still set from the above exports (or re-set them). You should also set ``SLEPC_DIR``: @@ -89,9 +94,7 @@ Now, you can install everything by simply running cd dynamite pip install ./ -Now you should be all set to use dynamite! If you want to work on the dynamite -source code, or just easily pull updates from GitHub, you might want to do -``pip install -e ./`` to keep the source files in-place. +Now you should be all set to use dynamite! .. note:: diff --git a/docs/rtd_requirements.txt b/docs/rtd_requirements.txt index 91f67c5..30d2473 100644 --- a/docs/rtd_requirements.txt +++ b/docs/rtd_requirements.txt @@ -1,30 +1,28 @@ alabaster==0.7.13 -Babel==2.11.0 -certifi==2022.12.7 -charset-normalizer==3.0.1 +Babel==2.13.1 +certifi==2023.7.22 +charset-normalizer==3.3.2 docutils==0.18.1 idna==3.4 imagesize==1.4.1 Jinja2==3.1.2 -markdown-it-py==2.1.0 -MarkupSafe==2.1.2 -mdit-py-plugins==0.3.3 +markdown-it-py==3.0.0 +MarkupSafe==2.1.3 +mdit-py-plugins==0.4.0 mdurl==0.1.2 -myst-parser==0.18.1 -packaging==23.0 -Pygments==2.14.0 -pytz==2022.7.1 -PyYAML==6.0 -requests==2.28.2 +myst-parser==2.0.0 +packaging==23.2 +Pygments==2.16.1 +PyYAML==6.0.1 +requests==2.31.0 snowballstemmer==2.2.0 -Sphinx==5.3.0 -sphinx-rtd-theme==1.2.0 -sphinxcontrib-applehelp==1.0.4 -sphinxcontrib-devhelp==1.0.2 -sphinxcontrib-htmlhelp==2.0.1 -sphinxcontrib-jquery==2.0.0 +Sphinx==7.2.6 +sphinx-rtd-theme==1.3.0 +sphinxcontrib-applehelp==1.0.7 +sphinxcontrib-devhelp==1.0.5 +sphinxcontrib-htmlhelp==2.0.4 +sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.3 -sphinxcontrib-serializinghtml==1.1.5 -typing_extensions==4.4.0 -urllib3==1.26.14 +sphinxcontrib-qthelp==1.0.6 +sphinxcontrib-serializinghtml==1.1.9 +urllib3==2.0.7 diff --git a/pyproject.toml b/pyproject.toml index 845531f..18b8650 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ "numpy", "scipy", "threadpoolctl", - "petsc4py ~= 3.19.0", - "slepc4py ~= 3.19.0", + "petsc4py == 3.20.1", + "slepc4py == 3.20.0", ] [build-system] diff --git a/src/dynamite/_backend/bsubspace.pyx b/src/dynamite/_backend/bsubspace.pyx index 870b7de..43f5c91 100644 --- a/src/dynamite/_backend/bsubspace.pyx +++ b/src/dynamite/_backend/bsubspace.pyx @@ -212,7 +212,7 @@ cdef extern int __builtin_parityl(unsigned long x) def compute_rcm(PetscInt [:] masks, PetscInt [:] signs, np.complex128_t [:] coeffs, PetscInt [:] state_map, PetscInt start, PetscInt L): - cdef PetscInt full_dim = 2**L + cdef PetscInt full_dim = pow(2, L) cdef PetscInt nnz = len(np.unique(masks)) cdef PetscInt map_idx, i, msc_idx, cur_mask, edge, sign, state cdef PetscInt nterms, max_states diff --git a/src/dynamite/subspaces.py b/src/dynamite/subspaces.py index a325497..e720f73 100644 --- a/src/dynamite/subspaces.py +++ b/src/dynamite/subspaces.py @@ -216,9 +216,9 @@ def _to_c(self): class Full(_ProductStateSubspace): _enum = bsubspace.SubspaceType.FULL - _c_get_dimension = bsubspace.get_dimension_Full - _c_idx_to_state = bsubspace.idx_to_state_Full - _c_state_to_idx = bsubspace.state_to_idx_Full + _c_get_dimension = staticmethod(bsubspace.get_dimension_Full) + _c_idx_to_state = staticmethod(bsubspace.idx_to_state_Full) + _c_state_to_idx = staticmethod(bsubspace.state_to_idx_Full) def __init__(self, L=None): super().__init__(L) @@ -260,9 +260,9 @@ class Parity(_ProductStateSubspace): ''' _enum = bsubspace.SubspaceType.PARITY - _c_get_dimension = bsubspace.get_dimension_Parity - _c_idx_to_state = bsubspace.idx_to_state_Parity - _c_state_to_idx = bsubspace.state_to_idx_Parity + _c_get_dimension = staticmethod(bsubspace.get_dimension_Parity) + _c_idx_to_state = staticmethod(bsubspace.idx_to_state_Parity) + _c_state_to_idx = staticmethod(bsubspace.state_to_idx_Parity) def __init__(self, space, L=None): super().__init__(L) @@ -319,9 +319,9 @@ class SpinConserve(_ProductStateSubspace): ''' _enum = bsubspace.SubspaceType.SPIN_CONSERVE - _c_get_dimension = bsubspace.get_dimension_SpinConserve - _c_idx_to_state = bsubspace.idx_to_state_SpinConserve - _c_state_to_idx = bsubspace.state_to_idx_SpinConserve + _c_get_dimension = staticmethod(bsubspace.get_dimension_SpinConserve) + _c_idx_to_state = staticmethod(bsubspace.idx_to_state_SpinConserve) + _c_state_to_idx = staticmethod(bsubspace.state_to_idx_SpinConserve) def __init__(self, L, k, spinflip=None): super().__init__(L=L) @@ -388,9 +388,9 @@ class Explicit(_ProductStateSubspace): ''' _enum = bsubspace.SubspaceType.EXPLICIT - _c_get_dimension = bsubspace.get_dimension_Explicit - _c_idx_to_state = bsubspace.idx_to_state_Explicit - _c_state_to_idx = bsubspace.state_to_idx_Explicit + _c_get_dimension = staticmethod(bsubspace.get_dimension_Explicit) + _c_idx_to_state = staticmethod(bsubspace.idx_to_state_Explicit) + _c_state_to_idx = staticmethod(bsubspace.state_to_idx_Explicit) def __init__(self, state_list, L=None): self.state_map = np.asarray(state_list, dtype=bsubspace.dnm_int_t) From da4d6222a415be1611e043b79d315ed05632c778 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 13:09:30 -1000 Subject: [PATCH 40/73] switch to CPU if GPU not found when using GPU build --- src/dynamite/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/dynamite/__init__.py b/src/dynamite/__init__.py index c6c44b4..2a05e5d 100644 --- a/src/dynamite/__init__.py +++ b/src/dynamite/__init__.py @@ -1,5 +1,6 @@ from os import environ +import subprocess import slepc4py from threadpoolctl import threadpool_limits from . import validate @@ -59,7 +60,28 @@ def _initialize(self, slepc_args=None, version_check=True, gpu=None): slepc_args = [] if gpu is None: - gpu = bbuild.have_gpu_shell() + if bbuild.have_gpu_shell(): + # check for a working GPU + try: + gpu_check = subprocess.run( + ['nvidia-smi'], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL + ) + except FileNotFoundError: + # nvidia-smi was not found + gpu_check = False + + gpu = gpu_check and gpu_check.returncode == 0 + if not gpu: + print('Warning: dynamite was built for GPU usage but failed to find either ' + 'the nvidia-smi command or the GPU itself. Switching to CPU.\n' + 'To force dynamite to attempt to use GPU, use ' + 'dynamite.config.initialize(gpu=True)\n' + 'To disable this warning, use ' + 'dynamite.config.initialize(gpu=False)') + + else: + gpu = False if gpu: if not bbuild.have_gpu_shell(): From 5f2e8731f411405a6c0691091b3c310f1b6767b5 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 13:10:22 -1000 Subject: [PATCH 41/73] replace NULL -> PETSC_NULLPTR --- src/dynamite/_backend/bcuda_impl.cu | 6 +++--- src/dynamite/_backend/bcuda_template_1.cu | 2 +- src/dynamite/_backend/bcuda_template_2.cu | 2 +- src/dynamite/_backend/bpetsc_template_1.c | 2 +- src/dynamite/_backend/bpetsc_template_2.c | 12 ++++++------ src/dynamite/_backend/bsubspace_impl.h | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/dynamite/_backend/bcuda_impl.cu b/src/dynamite/_backend/bcuda_impl.cu index 4f61a74..0cfed51 100644 --- a/src/dynamite/_backend/bcuda_impl.cu +++ b/src/dynamite/_backend/bcuda_impl.cu @@ -121,7 +121,7 @@ PetscErrorCode CopySubspaceData_CUDA_Explicit(data_Explicit** out_p, const data_ PetscCallCUDA(cudaMemcpy(cpu_data.state_map, in->state_map, sizeof(PetscInt)*in->dim, cudaMemcpyHostToDevice)); - if (in->rmap_indices != NULL) { + if (in->rmap_indices != PETSC_NULLPTR) { PetscCallCUDA(cudaMalloc(&(cpu_data.rmap_indices), sizeof(PetscInt)*in->dim)); PetscCallCUDA(cudaMemcpy(cpu_data.rmap_indices, in->rmap_indices, sizeof(PetscInt)*in->dim, cudaMemcpyHostToDevice)); @@ -143,7 +143,7 @@ PetscErrorCode DestroySubspaceData_CUDA_Explicit(data_Explicit* data) { PetscCallCUDA(cudaMemcpy(&cpu_data, data, sizeof(data_Explicit), cudaMemcpyDeviceToHost)); PetscCallCUDA(cudaFree(cpu_data.state_map)); - if (cpu_data.rmap_indices != NULL) { + if (cpu_data.rmap_indices != PETSC_NULLPTR) { PetscCallCUDA(cudaFree(cpu_data.rmap_indices)); } PetscCallCUDA(cudaFree(cpu_data.rmap_states)); @@ -160,7 +160,7 @@ __device__ PetscInt S2I_CUDA_Explicit(PetscInt state, const data_Explicit* data) while (left <= right) { mid = left + (right-left)/2; if (data->rmap_states[mid] == state) { - if (data->rmap_indices != NULL) { + if (data->rmap_indices != PETSC_NULLPTR) { return data->rmap_indices[mid]; } else { return mid; diff --git a/src/dynamite/_backend/bcuda_template_1.cu b/src/dynamite/_backend/bcuda_template_1.cu index 28c89d6..282fcc4 100644 --- a/src/dynamite/_backend/bcuda_template_1.cu +++ b/src/dynamite/_backend/bcuda_template_1.cu @@ -7,7 +7,7 @@ PetscErrorCode C(PrecomputeDiagonal_GPU,SUBSPACE)(Mat A) shell_context *ctx; PetscCall(MatShellGetContext(A, &ctx)); - PetscCall(MatGetSize(A, &size, NULL)); + PetscCall(MatGetSize(A, &size, PETSC_NULLPTR)); PetscCallCUDA(cudaMalloc((void **) &(ctx->diag), sizeof(PetscReal)*size)); diff --git a/src/dynamite/_backend/bcuda_template_2.cu b/src/dynamite/_backend/bcuda_template_2.cu index 9cf212b..84e660e 100644 --- a/src/dynamite/_backend/bcuda_template_2.cu +++ b/src/dynamite/_backend/bcuda_template_2.cu @@ -263,7 +263,7 @@ PetscErrorCode C(MatNorm_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, NormType ty PetscCallCUDA(cudaMalloc((void **) &d_maxs, sizeof(PetscReal)*GPU_BLOCK_NUM)); PetscCall(PetscMalloc1(GPU_BLOCK_NUM, &h_maxs)); - PetscCall(MatGetSize(A, &M, NULL)); + PetscCall(MatGetSize(A, &M, PETSC_NULLPTR)); C(device_MatNorm,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))<<>>( M, diff --git a/src/dynamite/_backend/bpetsc_template_1.c b/src/dynamite/_backend/bpetsc_template_1.c index e65c636..552f60b 100644 --- a/src/dynamite/_backend/bpetsc_template_1.c +++ b/src/dynamite/_backend/bpetsc_template_1.c @@ -175,7 +175,7 @@ PetscErrorCode C(PrecomputeDiagonal_CPU,SUBSPACE)(Mat A){ PetscCall(MatShellGetContext(A, &ctx)); if (ctx->masks[0] != 0) { - /* there is no diagonal! leave diag as NULL */ + /* there is no diagonal! leave diag as PETSC_NULLPTR */ return 0; } diff --git a/src/dynamite/_backend/bpetsc_template_2.c b/src/dynamite/_backend/bpetsc_template_2.c index 33073f5..fca3bd1 100644 --- a/src/dynamite/_backend/bpetsc_template_2.c +++ b/src/dynamite/_backend/bpetsc_template_2.c @@ -45,7 +45,7 @@ PetscErrorCode C(BuildMat,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( ctx->nmasks = msc->nmasks; ctx->nrm = -1; - ctx->diag = NULL; // diag is allocated later, if filled + ctx->diag = PETSC_NULLPTR; // diag is allocated later, if filled ctx->left_subspace_type = C(LEFT_SUBSPACE,ENUM); ctx->right_subspace_type = C(RIGHT_SUBSPACE,ENUM); } @@ -110,7 +110,7 @@ PetscErrorCode C(BuildPetsc,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( /* compute matrix elements */ PetscCall(MatSetOption(*A, MAT_NO_OFF_PROC_ENTRIES, PETSC_TRUE)); PetscCall(MatGetOwnershipRange(*A, &row_start, &row_end)); - PetscCall(MatGetOwnershipRangeColumn(*A, &col_start, NULL)); + PetscCall(MatGetOwnershipRangeColumn(*A, &col_start, PETSC_NULLPTR)); for (row_idx = row_start; row_idx < row_end; ++row_idx) { @@ -699,7 +699,7 @@ void C(compute_mask_starts,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( // in parity case, we drop the last bit of the mask (by calling S2I on it) while ( mask_idx < nmasks && - C(S2I_nocheck,LEFT_SUBSPACE)(masks[mask_idx], NULL) < (proc_idx << n_local_spins) + C(S2I_nocheck,LEFT_SUBSPACE)(masks[mask_idx], PETSC_NULLPTR) < (proc_idx << n_local_spins) ) { ++mask_idx; } @@ -793,7 +793,7 @@ PetscErrorCode C(MatMult_CPU_Fast,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, if (mask_starts[proc_idx] == ctx->nmasks) break; /* the first index of the target */ - m = C(S2I_nocheck,LEFT_SUBSPACE)(ctx->masks[mask_starts[proc_idx]], NULL); + m = C(S2I_nocheck,LEFT_SUBSPACE)(ctx->masks[mask_starts[proc_idx]], PETSC_NULLPTR); proc_start_idx = proc_mask & (proc_me ^ m); for (block_start_idx = proc_start_idx; @@ -828,7 +828,7 @@ PetscErrorCode C(MatMult_CPU_Fast,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, m = C(S2I_nocheck,LEFT_SUBSPACE)( ctx->masks[mask_idx], - NULL + PETSC_NULLPTR ); for ( @@ -838,7 +838,7 @@ PetscErrorCode C(MatMult_CPU_Fast,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, s = C(S2I_nocheck,LEFT_SUBSPACE)( ctx->signs[term_idx], - NULL + PETSC_NULLPTR ); ms_parity = builtin_parity(ctx->masks[mask_idx] & ctx->signs[term_idx]); diff --git a/src/dynamite/_backend/bsubspace_impl.h b/src/dynamite/_backend/bsubspace_impl.h index 0e51b0d..d60a270 100644 --- a/src/dynamite/_backend/bsubspace_impl.h +++ b/src/dynamite/_backend/bsubspace_impl.h @@ -278,7 +278,7 @@ static inline PetscErrorCode CopySubspaceData_Explicit(data_Explicit** out_p, co PetscCall(PetscMalloc1(in->dim, &((*out_p)->state_map))); PetscCall(PetscMemcpy((*out_p)->state_map, in->state_map, in->dim*sizeof(PetscInt))); - if (in->rmap_indices != NULL) { + if (in->rmap_indices != PETSC_NULLPTR) { PetscCall(PetscMalloc1(in->dim, &((*out_p)->rmap_indices))); PetscCall(PetscMemcpy((*out_p)->rmap_indices, in->rmap_indices, in->dim*sizeof(PetscInt))); } @@ -291,7 +291,7 @@ static inline PetscErrorCode CopySubspaceData_Explicit(data_Explicit** out_p, co static inline PetscErrorCode DestroySubspaceData_Explicit(data_Explicit* data) { PetscCall(PetscFree(data->state_map)); - if (data->rmap_indices != NULL) { + if (data->rmap_indices != PETSC_NULLPTR) { PetscCall(PetscFree(data->rmap_indices)); } PetscCall(PetscFree(data->rmap_states)); @@ -312,7 +312,7 @@ static inline PetscInt S2I_Explicit(PetscInt state, const data_Explicit* data) { while (left <= right) { mid = (left + right)/2; if (data->rmap_states[mid] == state) { - if (data->rmap_indices != NULL) { + if (data->rmap_indices != PETSC_NULLPTR) { return data->rmap_indices[mid]; } else { From 10cefe34f3c71c68de94c52244fa63188651f047 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 15:25:49 -1000 Subject: [PATCH 42/73] improve automatic version check --- CHANGELOG.md | 1 + src/dynamite/__init__.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c43fe..8f929fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Changed - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` - shell matrix-vector multiplications are now considerably faster + - Improved automatic version check; no longer leaves `.dynamite` files in working directory ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/src/dynamite/__init__.py b/src/dynamite/__init__.py index 2a05e5d..8ce6a3c 100644 --- a/src/dynamite/__init__.py +++ b/src/dynamite/__init__.py @@ -206,13 +206,13 @@ def check_version(): from urllib import request import json from os import remove - from os.path import isfile + from os.path import isfile, expanduser from time import time from sys import stderr # only check once a day for a new version so that we don't DOS GitHub # we save a file with the time of the last check in it - filename = '.dynamite' + filename = expanduser('~/.dynamite') if isfile(filename): with open(filename) as f: last_check = float(f.read().strip()) @@ -232,24 +232,20 @@ def check_version(): f.write(str(time())) remove(filename+'_lock') - # another process is doing this at the same time, - # or we don't have write permission here - except (FileExistsError, PermissionError, OSError): + # in general, catching all exceptions is a bad idea. but here, no matter + # what happens we just want to give up on the check + except: return # finally do the check - url = 'https://api.github.com/repos/GregDMeyer/dynamite/releases/latest' + url = 'https://raw.githubusercontent.com/GregDMeyer/dynamite/master/VERSION' try: with request.urlopen(url, timeout=1) as url_req: - data = json.load(url_req) - - # in general, catching all exceptions is a bad idea. but here, no matter - # what happens we just want to give up on the check + release_version = url_req.read().strip() except: return - release_version = data["tag_name"][1:] # tag_name starts with 'v' if release_version != bbuild.get_build_version(): print('A new version of dynamite has been released!', file=stderr) From 6b445fd3f18247fed8e5448feb3791229c7a69c0 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 15:26:36 -1000 Subject: [PATCH 43/73] update changelog following da4d6222a415be1611e043b79d315ed05632c778 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f929fb..c5cce8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` - shell matrix-vector multiplications are now considerably faster - Improved automatic version check; no longer leaves `.dynamite` files in working directory + - GPU builds now automatically switch to CPU if a GPU is not found (and print a warning) ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved From fa9ddef60cf5b46cc992ad992991c87ee7c802b6 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 15:29:15 -1000 Subject: [PATCH 44/73] update changelog following 197eec39d1d8dc62df6582c3f29b1e9bbb76a3dc --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cce8c..89fda86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved + - Build was broken with Cython 3 + - Work around broken `petsc4py` and `slepc4py` builds with `pip>=23.1` (see [PETSc issue](https://gitlab.com/petsc/petsc/-/issues/1369)) ## 0.3.1 - 2023-03-07 From 05a331cb6159ff6dceeda2c5f5662eca4b3417f3 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 14 Nov 2023 16:38:31 -1000 Subject: [PATCH 45/73] change default bind mount directory to container home --- CHANGELOG.md | 1 + docker/Dockerfile | 2 +- docs/containers.rst | 21 +++++++++++---------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89fda86..befefed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - shell matrix-vector multiplications are now considerably faster - Improved automatic version check; no longer leaves `.dynamite` files in working directory - GPU builds now automatically switch to CPU if a GPU is not found (and print a warning) + - Changed default bind mount location for Docker images to the container user's home directory, `/home/dnm` ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/docker/Dockerfile b/docker/Dockerfile index e0171c6..9d192e1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -202,8 +202,8 @@ COPY --chown=dnm:dnm examples /home/dnm/examples ENV DNM_DOCKER=1 # make work directory for mounting local files +# (now deprecated but we leave it for backwards compatibility) RUN mkdir -p /home/dnm/work # for permissions -VOLUME /home/dnm/work WORKDIR /home/dnm diff --git a/docs/containers.rst b/docs/containers.rst index d53ce12..2bed8ed 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -24,16 +24,18 @@ With Docker or podman installed (see :ref:`setup` below), run .. code:: bash - docker run --rm -it -w /home/dnm/work -v $PWD:/home/dnm/work gdmeyer/dynamite python your_script.py + docker run --rm -it -v $PWD:/home/dnm gdmeyer/dynamite python your_script.py # or replace 'docker' with 'podman' if you are using that # for podman you may need to add "docker.io/" in front of "gdmeyer" in the command A quick explanation of the options: - ``--rm -it``: run interactively, and automatically remove the container when finished - - ``-w /home/dnm/work -v $PWD:/home/dnm/work``: mount the current working directory into the + - ``-v $PWD:/home/dnm``: mount the current working directory into the container---this lets dynamite see your script! If you need to give dynamite access to - another directory, be sure to add another ``-v`` command. + another directory, be sure to add another ``-v`` command. (However note that if you are + not on Linux, there is a CPU cost every time a mounted file is modified, either + by the host or container---so mounting e.g. your entire home directory is in general a bad idea). .. note:: On Windows, you need to replace ``$PWD`` with ``%cd%`` (or whatever Windows path you want to mount @@ -55,7 +57,7 @@ However, if you prefer to use the GUI, that works fine too. The only thing you must do from the command line is pull the image: ``docker pull gdmeyer/dynamite:latest``. Now in the "Images" tab, hover over the dynamite image you just pulled, and hit "Run". -Expand the "Optional Settings" menu, and under "Volumes", mount a directory on your computer ("Host Path") onto ``/home/dnm/work`` in the container ("Container Path"). +Expand the "Optional Settings" menu, and under "Volumes", mount a directory on your computer ("Host Path") onto ``/home/dnm`` in the container ("Container Path"). You can also give the container a name if you want (otherwise Docker will pick a random name for you). Then hit "Run"! @@ -114,11 +116,10 @@ Command line .. code:: bash - docker run --rm -p 8887:8887 -w /home/dnm/work -v $PWD:/home/dnm/work gdmeyer/dynamite:latest-jupyter + docker run --rm -p 8887:8887 -v $PWD:/home/dnm gdmeyer/dynamite:latest-jupyter # or replace 'docker' with 'podman' Then follow the last link that you see (it should start with ``http://127.0.0.1:8887``). -Your files will be in the ``work`` directory visible in JupyterLab. Docker Desktop -------------- @@ -198,20 +199,20 @@ Installing other packages in your container =========================================== If you want to install other Python packages or other software to use alongside dynamite, it is possible to do this with Docker. -However, it's a little annoying; if the extra software is for analysis or similar we recommend saving the output of your dynamite computation to a file in your mounted directory (e.g. ``/home/dnm/work``) and then performing the analysis after-the-fact. +However, it's a little annoying; if the extra software is for analysis or similar we recommend saving the output of your dynamite computation to a file in your mounted directory (e.g. ``/home/dnm``) and then performing the analysis after-the-fact. A quick explainer of what's happening here: when you run dynamite using the commands in the `Quick Usage Guide`_ section above, Docker creates a "container" on top of the dynamite image. With the ``--rm`` flag as described above, this container is simply removed when the program run inside docker exits. However, by removing the ``--rm`` flag (and perhaps adding a ``--name``), we can keep the container around, make changes, add things, etc. -So, to make a persistent container, which mounts the current directory at ``/home/dnm/work``, run dynamite like this: +So, to make a persistent container, which mounts the current directory at the container user's home directory ``/home/dnm``, run dynamite like this: .. code:: bash - docker run --name my_dnm_container -it -v $PWD:/home/dnm/work gdmeyer/dynamite bash + docker run --name my_dnm_container -it -v $PWD:/home/dnm gdmeyer/dynamite bash This will give you a bash shell, where you can run ``pip install `` or anything else you would like. -Note that the directory mount (the ``-v`` option) is a part of the container, so when you run the commands below the same directory will always be mounted at ``/home/dnm/work``. +Note that the directory mount (the ``-v`` option) is a part of the container, so when you run the commands below the same directory will always be mounted at ``/home/dnm``. After you exit the bash shell above, the next time you want to use the same container, run From 4db74c773333131848ce823ba428aea4cabdf9f0 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 16 Nov 2023 10:02:02 -1000 Subject: [PATCH 46/73] remove unused requirements.txt --- requirements.txt | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b07f568..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -Cython -numpy -scipy -threadpoolctl From 40974b67610a59ece67ad7558a21067760b42c9c Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Thu, 16 Nov 2023 10:54:50 -1000 Subject: [PATCH 47/73] rename eigsolve "which" values --- CHANGELOG.md | 1 + src/dynamite/computations.py | 31 +++++++++++++++++++++--------- src/dynamite/operators.py | 2 +- tests/integration/test_eigsolve.py | 18 ++++++++--------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index befefed..cd0e2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Improved automatic version check; no longer leaves `.dynamite` files in working directory - GPU builds now automatically switch to CPU if a GPU is not found (and print a warning) - Changed default bind mount location for Docker images to the container user's home directory, `/home/dnm` + - Renamed some values of `which` argument of `eigsolve()`: `smallest`→`lowest` and `largest`→`highest` ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/src/dynamite/computations.py b/src/dynamite/computations.py index 3208665..b4f33df 100644 --- a/src/dynamite/computations.py +++ b/src/dynamite/computations.py @@ -1,10 +1,12 @@ +import warnings +import numpy as np + from . import config from .states import State from .tools import complex_enabled from .msc_tools import dnm_int_t -import numpy as np def evolve(H, state, t, result=None, tol=None, ncv=None, algo=None, max_its=None): r""" @@ -123,11 +125,11 @@ def evolve(H, state, t, result=None, tol=None, ncv=None, algo=None, max_its=None return result -def eigsolve(H, getvecs=False, nev=1, which='smallest', target=None, tol=None, subspace=None, max_its=None): +def eigsolve(H, getvecs=False, nev=1, which='lowest', target=None, tol=None, subspace=None, max_its=None): r""" Solve for a subset of the eigenpairs of the Hamiltonian. - By default, solves for the eigenvalue with the smallest (most + By default, solves for the eigenvalue with the lowest (most negative) real part, e.g. the ground state. Which eigenvalues are sought and how many can be adjusted with the options. @@ -157,9 +159,9 @@ def eigsolve(H, getvecs=False, nev=1, which='smallest', target=None, tol=None, s which : str Which eigenvalues to seek. Options are\: - - ``"smallest"``, to find the eigenvalues with smallest real part (i.e. most negative) + - ``"lowest"``, to find the eigenvalues with lowest (most negative) real part - - ``"largest"``, to find the eigenvalues with largest real part (i.e. most positive) + - ``"highest"``, to find the eigenvalues with highest (most positive) real part - ``"exterior"``, to find eigenvalues largest in absolute magnitude @@ -224,11 +226,22 @@ def eigsolve(H, getvecs=False, nev=1, which='smallest', target=None, tol=None, s eps.setDimensions(nev) + if which in ['smallest', 'largest']: + warnings.warn( + 'Warning: values "smallest" and "largest" for eigsolve parameter "which" ' + 'are deprecated, and have been replaced by "lowest" and "highest" respectively.', + DeprecationWarning + ) + which = { + 'smallest': 'lowest', + 'largest': 'highest' + }[which] + eps.setWhichEigenpairs({ - 'smallest':SLEPc.EPS.Which.SMALLEST_REAL, - 'largest':SLEPc.EPS.Which.LARGEST_REAL, - 'exterior':SLEPc.EPS.Which.LARGEST_MAGNITUDE, - 'target':SLEPc.EPS.Which.TARGET_MAGNITUDE, + 'lowest': SLEPc.EPS.Which.SMALLEST_REAL, + 'highest': SLEPc.EPS.Which.LARGEST_REAL, + 'exterior': SLEPc.EPS.Which.LARGEST_MAGNITUDE, + 'target': SLEPc.EPS.Which.TARGET_MAGNITUDE, }[which]) eps.setTolerances(tol=tol, max_it=max_its) diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index efecfd6..4041926 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -102,7 +102,7 @@ def eigsolve(self, **kwargs): method is a wrapper on :meth:`dynamite.computations.eigsolve`. Any keyword arguments are passed to that function; see its documentation for details. - By default, finds one (or possibly a few) eigenvalues with the smallest real + By default, finds one (or possibly a few) eigenvalues with the lowest values (i.e. the ground state). .. note:: The spin chain length ``L`` must be set before calling ``eigsolve``. diff --git a/tests/integration/test_eigsolve.py b/tests/integration/test_eigsolve.py index 83f4efa..b9217fb 100644 --- a/tests/integration/test_eigsolve.py +++ b/tests/integration/test_eigsolve.py @@ -100,22 +100,22 @@ def test_evals_only(self): def test_uniform_field(self): H = index_sum(sigmax()) - with self.subTest(which = 'smallest'): + with self.subTest(which = 'lowest'): nev = 2 evals, evecs = H.eigsolve(nev=nev, getvecs=True, - which='smallest', + which='lowest', tol=1E-10) evals_correct = [-H.get_length() + 2*i for i in range(nev)] for i in range(nev): self.is_close(evals[i], evals_correct[i]) self.check_is_evec(H, evecs[i], evals[i], tol=1E-10, evec_tol=1E-9) - with self.subTest(which = 'largest'): + with self.subTest(which = 'highest'): nev = 2 evals, evecs = H.eigsolve(nev=nev, getvecs=True, - which='largest', + which='highest', tol=1E-10) evals_correct = [H.get_length() - 2*i for i in range(nev)] for i in range(nev): @@ -124,7 +124,7 @@ def test_uniform_field(self): class Hamiltonians(Checker): - def test_all_smallest(self): + def test_all_lowest(self): for H_name in hamiltonians.get_names(complex_enabled()): if H_name == 'syk' and 'small_only' in self.skip_flags: continue @@ -132,7 +132,7 @@ def test_all_smallest(self): with self.subTest(H=H_name): H = getattr(hamiltonians, H_name)() - with self.subTest(which='smallest'): + with self.subTest(which='lowest'): evals, evecs = H.eigsolve(nev=5, getvecs=True, tol=1E-12) self.check_all(H, evals, evecs, tol=1E-12, evec_tol=1E-11) @@ -144,8 +144,8 @@ def test_all_target(self): with self.subTest(H=H_name): H = getattr(hamiltonians, H_name)() - lowest_eval = H.eigsolve(which='smallest')[0] - highest_eval = H.eigsolve(which='largest')[0] + lowest_eval = H.eigsolve(which='lowest')[0] + highest_eval = H.eigsolve(which='highest')[0] self.assertLess(lowest_eval, highest_eval) @@ -157,7 +157,7 @@ def test_all_target(self): class ZeroDiagonal(Checker): - def test_smallest(self): + def test_lowest(self): H = op_sum(0.1*i*sigmax(i) for i in range(config.L)) evals, evecs = H.eigsolve(nev=5, getvecs=True, tol=1E-12) self.check_all(H, evals, evecs, tol=1E-11, evec_tol=1E-9) From ef36548b740ea0d0619951c806d91e1cb9fca64c Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 1 Dec 2023 11:58:54 -1000 Subject: [PATCH 48/73] add entropy member function to State class --- CHANGELOG.md | 1 + examples/scripts/MBL/run_mbl.py | 3 +-- src/dynamite/computations.py | 5 +++-- src/dynamite/states.py | 4 +++- tests/integration/test_rdm.py | 4 ++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0e2dd..e72d99b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - `Operator.expectation()`, convenience function to compute the expectation value of the operator with respect to a state - `dynamite.tools.MPI_COMM_WORLD()` which returns PETSc's MPI communicator object - `Operator.precompute_diagonal` flag allows user to tune whether the matrix diagonal should be precomputed and saved, for shell matrices + - `State.entanglement_entropy` member function (a more convenient way of using `computations.entanglement_entropy`, which also remains) ### Changed - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` diff --git a/examples/scripts/MBL/run_mbl.py b/examples/scripts/MBL/run_mbl.py index 42315ea..6156941 100644 --- a/examples/scripts/MBL/run_mbl.py +++ b/examples/scripts/MBL/run_mbl.py @@ -4,7 +4,6 @@ from dynamite import config from dynamite.operators import sigmax, sigmay, sigmaz, index_sum from dynamite.subspaces import SpinConserve -from dynamite.computations import entanglement_entropy from dynamite.tools import mpi_print, MPI_COMM_WORLD import numpy as np @@ -87,7 +86,7 @@ def print_eig_stats(evals, evecs, h, energy_point): # sum the entropy for all evecs then divide by nev for the mean # NOTE: entanglement_entropy returns the EE value only on MPI rank 0, and -1 on all other ranks. # this is OK here because mpi_print below only prints on rank 0 - entropy = sum(entanglement_entropy(v, keep=range(config.L//2)) for v in evecs) + entropy = sum(v.entanglement_entropy(keep=range(config.L//2)) for v in evecs) entropy /= len(evecs) # compute the adjacent gap ratio of the eigenvals diff --git a/src/dynamite/computations.py b/src/dynamite/computations.py index b4f33df..59038a5 100644 --- a/src/dynamite/computations.py +++ b/src/dynamite/computations.py @@ -3,7 +3,6 @@ import numpy as np from . import config -from .states import State from .tools import complex_enabled from .msc_tools import dnm_int_t @@ -74,6 +73,7 @@ def evolve(H, state, t, result=None, tol=None, ncv=None, algo=None, max_its=None 'subspaces.') if result is None: + from .states import State # avoids circular import result = State(L=H.L, subspace=state.subspace) elif state.subspace != result.subspace: raise ValueError('input and result states are on different subspaces.') @@ -274,6 +274,7 @@ def eigsolve(H, getvecs=False, nev=1, which='lowest', target=None, tol=None, sub for i in range(nconv): evals[i] = eps.getEigenpair(i, None).real if getvecs: + from .states import State # avoids circular import v = State(L=H.L, subspace=H.subspace) eps.getEigenpair(i, v.vec) v.set_initialized() @@ -357,7 +358,7 @@ def entanglement_entropy(state, keep): A dynamite State object. keep : array-like - A list of spin indices to keep. See :meth:`reduced_density_matrix` for + A list of spin indices to keep. See :meth:`dynamite.computations.reduced_density_matrix` for details. Returns diff --git a/src/dynamite/states.py b/src/dynamite/states.py index 0faaa61..e57e656 100644 --- a/src/dynamite/states.py +++ b/src/dynamite/states.py @@ -1,5 +1,5 @@ -from . import config, validate, subspaces +from . import config, validate, subspaces, computations from .tools import complex_enabled, MPI_COMM_WORLD from .msc_tools import dnm_int_t @@ -359,6 +359,8 @@ def set_all_by_function(self, val_fn, vectorize=False): self.set_initialized() + entanglement_entropy = computations.entanglement_entropy + def project(self, index, value): ''' Project the state onto a subspace in which the qubit diff --git a/tests/integration/test_rdm.py b/tests/integration/test_rdm.py index ed02a3d..04456a3 100644 --- a/tests/integration/test_rdm.py +++ b/tests/integration/test_rdm.py @@ -11,7 +11,7 @@ from dynamite import config from dynamite.subspaces import Parity, Auto, SpinConserve, XParity from dynamite.states import State -from dynamite.computations import reduced_density_matrix, entanglement_entropy, renyi_entropy +from dynamite.computations import reduced_density_matrix, renyi_entropy from dynamite.tools import complex_enabled class Explicit(dtr.DynamiteTestCase): @@ -38,7 +38,7 @@ def check_entropy(self, state, keep, ent_entropy): config._initialize() from petsc4py import PETSc - check = entanglement_entropy(state, keep) + check = state.entanglement_entropy(keep) renyi_check = [] renyi_powers = [0, 1] From 2b238e6c7cb193e504bc8b09f140bd2842e9de83 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 1 Dec 2023 12:21:56 -1000 Subject: [PATCH 49/73] remove track_memory flag from benchmark --- CHANGELOG.md | 3 +++ benchmarking/benchmark.py | 34 +++++++++++++++------------------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e72d99b..78bf696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ - `Operator.precompute_diagonal` flag allows user to tune whether the matrix diagonal should be precomputed and saved, for shell matrices - `State.entanglement_entropy` member function (a more convenient way of using `computations.entanglement_entropy`, which also remains) +### Removed + - `--track_memory` flag to `benchmark.py`---now memory usage is always reported by the benchmarking script + ### Changed - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` - shell matrix-vector multiplications are now considerably faster diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index 5421d15..7741034 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -34,8 +34,6 @@ def parse_args(argv=None): parser.add_argument('--slepc_args', type=str, default='', help='Arguments to pass to SLEPc.') - parser.add_argument('--track_memory', action='store_true', - help='Whether to compute max memory usage (summed across all ranks)') parser.add_argument('--subspace', choices=['full', 'parity', 'spinconserve', @@ -241,8 +239,7 @@ def main(): for k,v in vars(arg_params).items(): mpi_print(str(k)+','+str(v)) - if arg_params.track_memory: - track_memory() + track_memory() stats = {} @@ -301,21 +298,20 @@ def main(): if arg_params.check_conserves: log_call(do_check_conserves, stats)(H) - if arg_params.track_memory: - # trigger memory measurement - # TODO: is this still required? - if H is not None: - H.destroy_mat() - elif in_state is not None: - in_state.vec.destroy() - - # sum the memory usage from all ranks - local_mem = get_max_memory_usage() - if MPI_COMM_WORLD().size == 1: - stats['Gb_memory'] = local_mem - else: - comm = MPI_COMM_WORLD().tompi4py() - stats['Gb_memory'] = comm.allreduce(local_mem) + # trigger memory measurement + # TODO: is this still required? + if H is not None: + H.destroy_mat() + elif in_state is not None: + in_state.vec.destroy() + + # sum the memory usage from all ranks + local_mem = get_max_memory_usage() + if MPI_COMM_WORLD().size == 1: + stats['Gb_memory'] = local_mem + else: + comm = MPI_COMM_WORLD().tompi4py() + stats['Gb_memory'] = comm.allreduce(local_mem) stats['total_time'] = default_timer() - main_start From f724a6c97960f4492bf8b9bd123a96c8005040f3 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 1 Dec 2023 13:10:33 -1000 Subject: [PATCH 50/73] improve warnings --- src/dynamite/__init__.py | 16 +++++--- src/dynamite/computations.py | 5 ++- src/dynamite/operators.py | 73 +++++++++++------------------------- 3 files changed, 35 insertions(+), 59 deletions(-) diff --git a/src/dynamite/__init__.py b/src/dynamite/__init__.py index 8ce6a3c..a89ec71 100644 --- a/src/dynamite/__init__.py +++ b/src/dynamite/__init__.py @@ -1,5 +1,6 @@ from os import environ +from sys import stderr import subprocess import slepc4py from threadpoolctl import threadpool_limits @@ -73,12 +74,15 @@ def _initialize(self, slepc_args=None, version_check=True, gpu=None): gpu = gpu_check and gpu_check.returncode == 0 if not gpu: - print('Warning: dynamite was built for GPU usage but failed to find either ' - 'the nvidia-smi command or the GPU itself. Switching to CPU.\n' - 'To force dynamite to attempt to use GPU, use ' - 'dynamite.config.initialize(gpu=True)\n' - 'To disable this warning, use ' - 'dynamite.config.initialize(gpu=False)') + print( + 'WARNING: dynamite was built for GPU usage but failed to find either ' + 'the nvidia-smi command or the GPU itself. Switching to CPU.\n' + 'To force dynamite to attempt to use GPU, use ' + 'dynamite.config.initialize(gpu=True)\n' + 'To disable this warning, use ' + 'dynamite.config.initialize(gpu=False)', + file=stderr + ) else: gpu = False diff --git a/src/dynamite/computations.py b/src/dynamite/computations.py index 59038a5..7722fe9 100644 --- a/src/dynamite/computations.py +++ b/src/dynamite/computations.py @@ -228,9 +228,10 @@ def eigsolve(H, getvecs=False, nev=1, which='lowest', target=None, tol=None, sub if which in ['smallest', 'largest']: warnings.warn( - 'Warning: values "smallest" and "largest" for eigsolve parameter "which" ' + 'values "smallest" and "largest" for eigsolve parameter "which" ' 'are deprecated, and have been replaced by "lowest" and "highest" respectively.', - DeprecationWarning + DeprecationWarning, + stacklevel=2 ) which = { 'smallest': 'lowest', diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index 4041926..185038b 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -7,6 +7,7 @@ from zlib import crc32 import re from string import ascii_lowercase +import warnings import numpy as np from . import config, validate, msc_tools @@ -68,52 +69,9 @@ def copy(self): return rtn ### computations - - def evolve(self, state, t, **kwargs): - r""" - Time-evolve a state, using the operator as the Hamiltonian. - - This method wraps :meth:`dynamite.computations.evolve` (see that documentation - for a full description of the method's functionality). - - Parameters - ---------- - state : dynamite.states.State - The initial state. - - t : float - The time :math:`t` for which to evolve the state (can be negative or complex). - - **kwargs : - Any further keyword arguments are passed to the underlying call to - :meth:`dynamite.computations.evolve`. See that documentation for a - detailed description of possible arguments. - - Returns - ------- - dynamite.states.State - The result vector :math:`\Psi_f`. - """ - return evolve(self, state, t, **kwargs) - - def eigsolve(self, **kwargs): - """ - Find eigenvalues (and eigenvectors if requested) of the Hamiltonian. This class - method is a wrapper on :meth:`dynamite.computations.eigsolve`. Any keyword - arguments are passed to that function; see its documentation for details. - - By default, finds one (or possibly a few) eigenvalues with the lowest - values (i.e. the ground state). - - .. note:: The spin chain length ``L`` must be set before calling ``eigsolve``. - - Returns - ------- - numpy.array or tuple(numpy.array, list(dynamite.states.State)) - Either a 1D numpy array of eigenvalues, or a pair containing that array - and a list of the corresponding eigenvectors. - """ - return eigsolve(self, **kwargs) + # directly uses the definitions from computations.py + evolve = evolve + eigsolve = eigsolve ### properties @@ -221,7 +179,12 @@ def msc_size(self): """ (deprecated) """ - raise DeprecationWarning('Operator.msc_size is deprecated, use Operator.nterms instead') + warnings.warn( + 'Operator.msc_size is deprecated, use Operator.nterms instead', + DeprecationWarning, + stacklevel=2 + ) + return self.nterms @property def density(self): @@ -1276,16 +1239,24 @@ def load_from_file(filename): ''' DEPRECATED: use dynamite.operators.Operator.load ''' - raise DeprecationWarning("operators.load_from_file is deprecated; " - "use operators.Operator.load") + warnings.warn( + "operators.load_from_file is deprecated; use operators.Operator.load", + DeprecationWarning, + stacklevel=2 + ) + return Operator.load(filename) def from_bytes(data): """ DEPRECATED: use dynamite.operators.Operator.from_bytes """ - raise DeprecationWarning("operators.from_bytes is deprecated; " - "use operators.Operator.from_bytes") + warnings.warn( + "operators.from_bytes is deprecated; use operators.Operator.from_bytes", + DeprecationWarning, + stacklevel=2 + ) + return Operator.from_bytes(data) def op_sum(terms, nshow = 3): From fc410f4eb1a4d9c5099e0822d23136e3271842e1 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 1 Dec 2023 17:22:07 -1000 Subject: [PATCH 51/73] improve memory usage tracking --- CHANGELOG.md | 2 + benchmarking/benchmark.py | 11 +-- examples/tutorial/6-ShellMatrices.ipynb | 10 +-- src/dynamite/_backend/bpetsc.pyx | 34 ++------ src/dynamite/tools.py | 100 +++++++++++++++++------- tests/integration/test_matrices.py | 37 ++++----- tests/integration/test_tools.py | 9 ++- 7 files changed, 107 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78bf696..5b4320c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ - `dynamite.tools.MPI_COMM_WORLD()` which returns PETSc's MPI communicator object - `Operator.precompute_diagonal` flag allows user to tune whether the matrix diagonal should be precomputed and saved, for shell matrices - `State.entanglement_entropy` member function (a more convenient way of using `computations.entanglement_entropy`, which also remains) + - `tools.get_memory_usage` which can measure memory usage on a total, per rank, or per node basis ### Removed - `--track_memory` flag to `benchmark.py`---now memory usage is always reported by the benchmarking script + - `tools.get_max_memory_usage` and `tools.get_cur_memory_usage` in favor of a single function `tools.get_memory_usage` ### Changed - `Operator.msc_size` renamed to `Operator.nterms`, and now invokes a call to `Operator.reduce_msc()` diff --git a/benchmarking/benchmark.py b/benchmarking/benchmark.py index 7741034..0fa60a0 100644 --- a/benchmarking/benchmark.py +++ b/benchmarking/benchmark.py @@ -11,7 +11,7 @@ from dynamite.operators import op_sum, op_product, index_sum from dynamite.extras import majorana from dynamite.subspaces import Full, Parity, SpinConserve, Auto, XParity -from dynamite.tools import track_memory, get_max_memory_usage, MPI_COMM_WORLD, mpi_print +from dynamite.tools import track_memory, get_memory_usage, mpi_print from dynamite.computations import reduced_density_matrix @@ -299,20 +299,13 @@ def main(): log_call(do_check_conserves, stats)(H) # trigger memory measurement - # TODO: is this still required? if H is not None: H.destroy_mat() elif in_state is not None: in_state.vec.destroy() # sum the memory usage from all ranks - local_mem = get_max_memory_usage() - if MPI_COMM_WORLD().size == 1: - stats['Gb_memory'] = local_mem - else: - comm = MPI_COMM_WORLD().tompi4py() - stats['Gb_memory'] = comm.allreduce(local_mem) - + stats['Gb_memory'] = get_memory_usage(group_by='all', max_usage=True) stats['total_time'] = default_timer() - main_start mpi_print('---RESULTS---') diff --git a/examples/tutorial/6-ShellMatrices.ipynb b/examples/tutorial/6-ShellMatrices.ipynb index 9984c8f..2fcce9a 100644 --- a/examples/tutorial/6-ShellMatrices.ipynb +++ b/examples/tutorial/6-ShellMatrices.ipynb @@ -105,16 +105,16 @@ }, "outputs": [], "source": [ - "from dynamite.tools import get_cur_memory_usage\n", + "from dynamite.tools import get_memory_usage\n", "from timeit import timeit\n", "\n", "config.L = 18\n", "\n", "H = XXZ()\n", "\n", - "before = get_cur_memory_usage()\n", + "before = get_memory_usage()\n", "duration = timeit(H.build_mat, number=1, globals=globals())\n", - "after = get_cur_memory_usage()\n", + "after = get_memory_usage()\n", "\n", "print(f'matrix memory usage: {after-before} Gb')\n", "print(f'matrix build time: {duration} s')" @@ -137,9 +137,9 @@ "source": [ "H.shell = True\n", "\n", - "before = get_cur_memory_usage()\n", + "before = get_memory_usage()\n", "duration = timeit(H.build_mat, number=1, globals=globals())\n", - "after = get_cur_memory_usage()\n", + "after = get_memory_usage()\n", "\n", "print(f'matrix memory usage: {after-before} Gb')\n", "print(f'matrix build time: {duration} s')" diff --git a/src/dynamite/_backend/bpetsc.pyx b/src/dynamite/_backend/bpetsc.pyx index c8b6792..a4d9861 100644 --- a/src/dynamite/_backend/bpetsc.pyx +++ b/src/dynamite/_backend/bpetsc.pyx @@ -201,21 +201,13 @@ def track_memory(): if ierr != 0: raise Error(ierr) -def get_max_memory_usage(which='all'): +def get_max_memory_usage(): ''' Get the maximum memory usage up to this point. Only updated whenever objects are destroyed (i.e. with :meth:`dynamite.operators.Operator.destroy_mat`) ..note :: - :meth:`track_memory` must be called before this function is called, - and the option `'-malloc'` must be supplied to PETSc at runtime to track - PETSc memory allocations - - Parameters - ---------- - which : str - `'all'` to return all memory usage for the process, `'petsc'` to return - only memory allocated by PETSc. + :meth:`track_memory` must be called before this function is called Returns ------- @@ -225,27 +217,16 @@ def get_max_memory_usage(which='all'): cdef int ierr cdef PetscLogDouble mem - if which == 'all': - ierr = PetscMemoryGetMaximumUsage(&mem) - elif which == 'petsc': - ierr = PetscMallocGetMaximumUsage(&mem) - else: - raise ValueError("argument 'which' must be 'all' or 'petsc'") + ierr = PetscMemoryGetMaximumUsage(&mem) if ierr != 0: raise Error(ierr) return mem -def get_cur_memory_usage(which='all'): +def get_cur_memory_usage(): ''' Get the current memory usage (resident set size) in bytes. - Parameters - ---------- - type : str - 'all' to return all memory usage for the process, 'petsc' to return - only memory allocated by PETSc. - Returns ------- float @@ -254,12 +235,7 @@ def get_cur_memory_usage(which='all'): cdef int ierr cdef PetscLogDouble mem - if which == 'all': - ierr = PetscMemoryGetCurrentUsage(&mem) - elif which == 'petsc': - ierr = PetscMallocGetCurrentUsage(&mem) - else: - raise ValueError("argument 'which' must be 'all' or 'petsc'") + ierr = PetscMemoryGetCurrentUsage(&mem) if ierr != 0: raise Error(ierr) diff --git a/src/dynamite/tools.py b/src/dynamite/tools.py index 03c6e1d..bfae628 100644 --- a/src/dynamite/tools.py +++ b/src/dynamite/tools.py @@ -2,6 +2,8 @@ Various tools useful for writing and analyzing dynamite programs. ''' +import warnings + def MPI_COMM_WORLD(): ''' @@ -83,7 +85,7 @@ def get_version_str(): def track_memory(): ''' - Begin tracking memory usage for a later call to :meth:`get_max_memory_usage`. + Begin tracking memory usage for a later call to ``get_memory_usage(..., max_usage=True)``. ''' from . import config config._initialize() @@ -91,53 +93,95 @@ def track_memory(): return bpetsc.track_memory() -def get_max_memory_usage(which='all'): +def get_memory_usage(group_by='all', max_usage=False): ''' - Get the maximum memory usage up to this point, in gigabytes. - Only updated whenever objects are destroyed (e.g. with - :meth:`dynamite.operators.Operator.destroy_mat`) + Get the memory usage, in gigabytes. + + .. note:: + :meth:`track_memory` must be called before this function is called + with ``max_usage=True``. .. note:: - :meth:`track_memory` must be called before this function is called, - and the option ``'-malloc'`` must be supplied to PETSc at runtime if - ``which == 'petsc'``. + Grouping by node only works if MPI is configured to allow shared memory between ranks on + the same node. If it is not, it may consider each rank its own "node." Whether this is the + case can be seen by observing whether the value returned by this function is identical for + all ranks on the same node, or if it is instead the same as the value returned for + ``group_by='rank'``. Parameters ---------- - which : str - ``'all'`` to return all memory usage for the process, ``'petsc'`` to return - only memory allocated by PETSc. + group_by : str + What ranks to sum memory usage over. Options are "rank", which will return each rank's + individual memory usage (which may be different across ranks); "node", which will sum + over ranks sharing the same memory (and thus again the result may differ between + ranks); and "all", which returns the total memory usage of all ranks. + + max_usage : bool + Instead of current memory usage, report maximum since the call to :meth:`track_memory()`. + Note that maximum is only updated when PETSc objects are destroyed, which may be delayed + due to garbage collection. Returns ------- float - The max memory usage in gigabytes + The memory usage in gigabytes ''' from . import config config._initialize() from ._backend import bpetsc - return bpetsc.get_max_memory_usage(which=which)/1E9 + if max_usage: + local_usage = bpetsc.get_max_memory_usage()/1E9 + else: + local_usage = bpetsc.get_cur_memory_usage()/1E9 -def get_cur_memory_usage(which='all'): + comm = MPI_COMM_WORLD() + if group_by == 'rank' or comm.size == 1: + return local_usage + + import mpi4py + comm = comm.tompi4py() + + if group_by == 'node': + split_comm = comm.Split_type(mpi4py.MPI.COMM_TYPE_SHARED) + elif group_by == 'all': + split_comm = comm + else: + raise ValueError(f"group_by must be 'rank', 'node', or 'all'; got '{group_by}'") + + return split_comm.allreduce(local_usage) + + +def get_max_memory_usage(which='all'): ''' - Get the current memory usage (resident set size) in gigabytes. + [deprecated] + ''' + if which != 'all': + raise ValueError('values of "which" other than "all" no longer supported') - Parameters - ---------- - type : str - ``'all'`` to return all memory usage for the process, ``'petsc'`` to return - only memory allocated by PETSc. + warnings.warn( + "get_max_memory_usage() is deprecated; use get_memory_usage(max_usage=True) instead", + DeprecationWarning, + stacklevel=2 + ) - Returns - ------- - float - The max memory usage in gigabytes + return get_memory_usage(group_by='rank', max_usage=True) + + +def get_cur_memory_usage(which='all'): ''' - from . import config - config._initialize() - from ._backend import bpetsc - return bpetsc.get_cur_memory_usage(which=which)/1E9 + [deprecated] + ''' + if which != 'all': + raise ValueError('values of "which" other than "all" no longer supported') + + warnings.warn( + "get_cur_memory_usage() is deprecated; use get_memory_usage() instead", + DeprecationWarning, + stacklevel=2 + ) + + return get_memory_usage(group_by='rank') def complex_enabled(): diff --git a/tests/integration/test_matrices.py b/tests/integration/test_matrices.py index b066224..662c0db 100644 --- a/tests/integration/test_matrices.py +++ b/tests/integration/test_matrices.py @@ -10,7 +10,7 @@ from dynamite import config from dynamite.states import State -from dynamite.tools import complex_enabled, get_cur_memory_usage +from dynamite.tools import complex_enabled, get_memory_usage from dynamite.operators import identity, sigmax, sigmay, index_sum from dynamite.msc_tools import msc_dtype, dnm_int_t from dynamite.subspaces import Auto, SpinConserve, XParity @@ -232,25 +232,22 @@ def setUp(self): from petsc4py import PETSc self.mpi_size = PETSc.COMM_WORLD.size - def test_diagonal(self): - H = index_sum(sigmaz()) - mem_pre = get_cur_memory_usage(which='petsc') + def check_memory(self, H): + mem_pre = get_memory_usage() H.build_mat() - mem_post = get_cur_memory_usage(which='petsc') - self.assertGreater( - 1E-5 + 1.3*H.estimate_memory()/self.mpi_size, - mem_post-mem_pre + mem_post = get_memory_usage() + + # allow some small overhead per rank + self.assertLess( + abs(mem_post-mem_pre - H.estimate_memory()), + 0.05*self.mpi_size ) + def test_diagonal(self): + self.check_memory(index_sum(sigmaz())) + def test_XX(self): - H = index_sum(sigmax(0)+sigmax(1)) - mem_pre = get_cur_memory_usage(which='petsc') - H.build_mat() - mem_post = get_cur_memory_usage(which='petsc') - self.assertGreater( - 1E-5 + 1.3*H.estimate_memory()/self.mpi_size, - mem_post-mem_pre - ) + self.check_memory(index_sum(sigmax(0)+sigmax(1))) def test_XXYY_auto(self): for sort in (False, True): @@ -258,13 +255,7 @@ def test_XXYY_auto(self): H = index_sum(sigmax(0)*sigmax(1) + sigmay(0)*sigmay(1)) half = config.L//2 H.subspace = Auto(H, 'U'*half + 'D'*(config.L-half), sort=sort) - mem_pre = get_cur_memory_usage(which='petsc') - H.build_mat() - mem_post = get_cur_memory_usage(which='petsc') - self.assertGreater( - 1E-5 + 1.3*H.estimate_memory()/self.mpi_size, - mem_post-mem_pre - ) + self.check_memory(H) if __name__ == '__main__': diff --git a/tests/integration/test_tools.py b/tests/integration/test_tools.py index cf594e9..105200c 100644 --- a/tests/integration/test_tools.py +++ b/tests/integration/test_tools.py @@ -20,11 +20,16 @@ def test_version_str(self): self.assertTrue(isinstance(tools.get_version_str(), str)) def test_cur_memory(self): - self.assertTrue(isinstance(tools.get_cur_memory_usage(), float)) + self.assertTrue(isinstance(tools.get_memory_usage(group_by='all'), float)) + self.assertTrue(isinstance(tools.get_memory_usage(group_by='rank'), float)) + self.assertTrue(isinstance(tools.get_memory_usage(group_by='node'), float)) def test_max_memory(self): tools.track_memory() - self.assertTrue(isinstance(tools.get_max_memory_usage(), float)) + self.assertTrue(isinstance(tools.get_memory_usage(group_by='all', max_usage=True), float)) + self.assertTrue(isinstance(tools.get_memory_usage(group_by='rank', max_usage=True), float)) + self.assertTrue(isinstance(tools.get_memory_usage(group_by='node', max_usage=True), float)) + if __name__ == '__main__': dtr.main() From 4fe89ccc84c33ba4346aaa28cfc6fa3d88eb4e06 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 13 Feb 2024 13:52:05 -0500 Subject: [PATCH 52/73] bump PETSc/SLEPc version numbers --- docker/Dockerfile | 4 ++-- docs/install.rst | 4 ++-- pyproject.toml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9d192e1..68b38e0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,8 @@ ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' -ARG PETSC_VERSION=3.20.1 -ARG SLEPC_VERSION=3.20.0 +ARG PETSC_VERSION=3.20.4 +ARG SLEPC_VERSION=3.20.1 ARG CUDA_VERSION=12.2.2 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 diff --git a/docs/install.rst b/docs/install.rst index a78fe6a..2ab8e62 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -36,7 +36,7 @@ following. There is a configuration script that comes with dynamite which should .. code:: bash - git clone --depth 1 --branch v3.19.0 https://gitlab.com/petsc/petsc.git petsc + git clone --depth 1 --branch v3.20.4 https://gitlab.com/petsc/petsc.git petsc cd petsc python /petsc_config/complex-opt.py @@ -61,7 +61,7 @@ Now download and install SLEPc: .. code:: bash - git clone --depth 1 --branch v3.19.0 https://gitlab.com/slepc/slepc.git slepc + git clone --depth 1 --branch v3.20.1 https://gitlab.com/slepc/slepc.git slepc cd slepc ./configure diff --git a/pyproject.toml b/pyproject.toml index 18b8650..2cef16c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ "numpy", "scipy", "threadpoolctl", - "petsc4py == 3.20.1", - "slepc4py == 3.20.0", + "petsc4py == 3.20.4", + "slepc4py == 3.20.1", ] [build-system] From 1a46c962a7b820b9d0a121dcb0eb35531555fe51 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Tue, 13 Feb 2024 12:19:48 -1000 Subject: [PATCH 53/73] improve operator repr --- src/dynamite/msc_tools.py | 12 +- src/dynamite/operators.py | 479 +++++++++++++++++++++-------------- src/dynamite/validate.py | 2 +- tests/unit/test_operators.py | 73 ++++++ 4 files changed, 361 insertions(+), 205 deletions(-) diff --git a/src/dynamite/msc_tools.py b/src/dynamite/msc_tools.py index 56ec5ce..ba20ef4 100644 --- a/src/dynamite/msc_tools.py +++ b/src/dynamite/msc_tools.py @@ -132,14 +132,10 @@ def msc_sum(iterable): np.ndarray The sum as an MSC matrix ''' - iterable = iter(iterable) - # if iterable has zero items, return zero - try: - first = next(iterable) - except StopIteration: - return np.ndarray(0, dtype = msc_dtype) - - return np.hstack([first]+list(iterable)) + term_lst = list(iterable) + if not term_lst: + return np.ndarray(0, dtype=msc_dtype) + return np.hstack(term_lst) def msc_product(iterable): ''' diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index 185038b..0eb4610 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -25,14 +25,17 @@ class Operator: other functions in this module. """ - def __init__(self): + def __init__(self, msc=None, string_rep=None): self._max_spin_idx = None self._mats = {} - self._msc = None self._is_reduced = False self._shell = config.shell self._precompute_diagonal = True self._allow_projection = False + self._msc = None + + if msc is not None: + self.msc = msc if config.subspace is not None: self._subspaces = [(config.subspace, config.subspace)] @@ -42,7 +45,9 @@ def __init__(self): if config.L is not None: self.L = config.L - self._string_rep = _OperatorStringRep() + if string_rep is None: + string_rep = _OperatorStringRep() + self._string_rep = string_rep def copy(self): """ @@ -436,10 +441,7 @@ def __str__(self): return self._string_rep.string def __repr__(self): - rtn = f'= nshow: - break - else: - done = True - - if not done: - strings[-1] = '...' - texs[-1] = r'\cdots' - msc_terms.append(msc_tools.msc_sum(t.msc for t in iterterms)) + repr_strs.append(t._string_rep.repr_str) + if n < nshow: + strings.append(t._string_rep.string) + texs.append(t._string_rep.tex) + else: + add_ellipses = True + + if add_ellipses: + strings.append('...') + texs.append(r'\cdots') + + return Operator( + msc=msc_tools.msc_sum(msc_terms), + string_rep=_OperatorStringRep( + string=' + '.join(strings), + tex=' + '.join(texs), + repr_str=' + '.join(repr_strs), + brackets='()' + ) + ) - o.msc = msc_tools.msc_sum(msc_terms) - o._string_rep.string = ' + '.join(strings) - o._string_rep.tex = ' + '.join(texs) - o._string_rep.brackets = '()' - return o def op_product(terms): """ @@ -1332,23 +1241,30 @@ def op_product(terms): msc_terms = [] strings = [] texs = [] + repr_strs = [] for t in terms: msc_terms.append(t.msc) strings.append(t._string_rep.with_brackets('string')) texs.append(t._string_rep.with_brackets('tex')) + repr_strs.append(t._string_rep.with_brackets('repr')) if msc_terms: - o = Operator() - o.msc = msc_tools.msc_product(msc_terms) - o._string_rep.string = '*'.join(strings) - o._string_rep.tex = ''.join(texs) - o._string_rep.brackets = '' + rtn = Operator( + msc=msc_tools.msc_product(msc_terms), + string_rep=_OperatorStringRep( + string='*'.join(strings), + tex=''.join(texs), + repr_str='*'.join(repr_strs), + brackets = '' + ) + ) else: - o = identity() + rtn = identity() + + return rtn - return o -def index_sum(op, size = None, start = 0, boundary = 'open'): +def index_sum(op, size=None, start=0, boundary='open'): """ Duplicate the operator onto adjacent sites in the spin chain, and sum the resulting operators. @@ -1374,13 +1290,15 @@ def index_sum(op, size = None, start = 0, boundary = 'open'): on more than one site, this determines whether the last few terms of the sum should wrap around to the beginning of the spin chain. """ - if size is None: if op.L is None: raise ValueError('Must specify index_sum size with either the "size" argument ' 'or by setting Operator.L (possibly through config.L).') else: + default_size = True size = op.L + else: + default_size = False size = validate.L(size) @@ -1401,14 +1319,20 @@ def index_sum(op, size = None, start = 0, boundary = 'open'): else: raise ValueError("invalid value for argument 'boundary' (can be 'open' or 'closed')") - rtn = Operator() - rtn.msc = msc_tools.msc_sum(op.get_shifted_msc(i, wrap_idx) for i in range(start, stop)) + string_rep = _OperatorStringRep() - rtn._string_rep.string = 'index_sum(' + str(op) + ', sites %d - %d' % (start, stop-1) + string_rep.string = 'index_sum(' + str(op) + ', sites %d - %d' % (start, stop-1) + string_rep.repr_str = f'index_sum({repr(op)}' + if not default_size: + string_rep.repr_str += f', size={size}' + if start != 0: + string_rep.repr_str += f', start={start}' if boundary == 'closed': - rtn._string_rep.string += ', wrapped)' - else: - rtn._string_rep.string += ')' + string_rep.string += ', wrapped' + string_rep.repr_str += f', boundary="closed"' + + string_rep.string += ')' + string_rep.repr_str += ')' # add i to the indices for TeX representation sub_tex = op._string_rep.with_brackets('tex') @@ -1416,12 +1340,16 @@ def index_sum(op, size = None, start = 0, boundary = 'open'): sub_tex = sub_tex.replace('{IDX', '{IDX'+idx+'+') sub_tex = sub_tex.replace('{IDX'+idx+'+0', '{IDX'+idx) - rtn._string_rep.tex = r'\sum\limits_{'+idx+'=%d}^{%d}' % (start, stop-1) + sub_tex - rtn._string_rep.brackets = '[]' + string_rep.tex = r'\sum\limits_{'+idx+'=%d}^{%d}' % (start, stop-1) + sub_tex + string_rep.brackets = '[]' - return rtn + return Operator( + msc=msc_tools.msc_sum(op.get_shifted_msc(i, wrap_idx) for i in range(start, stop)), + string_rep=string_rep + ) -def index_product(op, size = None, start = 0): + +def index_product(op, size=None, start=0): """ Duplicate the operator onto adjacent sites in the spin chain, and multiply the resulting operators together. @@ -1445,7 +1373,10 @@ def index_product(op, size = None, start = 0): raise ValueError('Must specify index_sum size with either the "size" argument ' 'or by setting Operator.L (possibly through config.L).') else: + default_size = True size = op.L + else: + default_size = False if size == 0: return identity() @@ -1454,21 +1385,30 @@ def index_product(op, size = None, start = 0): stop = start + size - op.max_spin_idx - rtn = Operator() - rtn.msc = msc_tools.msc_product(op.get_shifted_msc(i, wrap_idx = None) for i in range(start, stop)) + string_rep = _OperatorStringRep( + string='index_product(' + str(op) + ', sites %d - %d)' % (start, stop-1) + ) - rtn._string_rep.string = 'index_product(' + str(op) + ', sites %d - %d)' % (start, stop-1) + string_rep.repr_str = f'index_product({repr(op)}' + if not default_size: + string_rep.repr_str += f', size={size}' + if start != 0: + string_rep.repr_str += f', start={start}' + string_rep.repr_str += ')' # add i to the indices for TeX representation sub_tex = op._string_rep.with_brackets('tex') idx = _get_next_index(sub_tex) sub_tex = sub_tex.replace('{IDX', '{IDX'+idx+'+') sub_tex = sub_tex.replace('{IDX'+idx+'+0', '{IDX'+idx) - rtn._string_rep.tex = r'\prod\limits_{'+idx+'=%d}^{%d}' % (start, stop-1) - rtn._string_rep.tex += sub_tex - rtn._string_rep.brackets = '[]' + string_rep.tex = r'\prod\limits_{'+idx+'=%d}^{%d}' % (start, stop-1) + string_rep.tex += sub_tex + string_rep.brackets = '[]' - return rtn + return Operator( + msc=msc_tools.msc_product(op.get_shifted_msc(i, wrap_idx=None) for i in range(start, stop)), + string_rep=string_rep + ) def _get_next_index(tex_str): @@ -1490,11 +1430,15 @@ def sigmax(i=0): """ i = validate.spin_index(i) - o = Operator() - o.msc = [(1< Date: Wed, 14 Feb 2024 10:49:23 -1000 Subject: [PATCH 54/73] tests and fixes for extras module --- src/dynamite/extras.py | 12 ++++++--- tests/unit/test_extras.py | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_extras.py diff --git a/src/dynamite/extras.py b/src/dynamite/extras.py index 058139a..6450819 100644 --- a/src/dynamite/extras.py +++ b/src/dynamite/extras.py @@ -1,7 +1,8 @@ from .operators import sigmax, sigmay, sigmaz, index_product -def commutator(o1, o2): + +def commutator(op1, op2): """ The commutator :math:`[O_1,O_2]`. @@ -10,12 +11,14 @@ def commutator(o1, o2): dynamite.operators.Operator The commutator """ - rtn = o1*o2 - o2*o1 - rtn._string_rep.string = '[%s, %s]' % (o1.string, o2.string) - rtn._string_rep.tex = r'\left[ %s, %s \right]' % (o1.tex, o2.tex) + rtn = op1*op2 - op2*op1 + rtn._string_rep.string = f'[{op1}, {op2}]' + rtn._string_rep.tex = r'\left[ %s, %s \right]' % (op1._string_rep.tex, op2._string_rep.tex) + rtn._string_rep.repr_str = f'commutator({repr(op1)}, {repr(op2)})' rtn._string_rep.brackets = '' return rtn + def majorana(idx): r""" A function generating an operator that represents a @@ -50,6 +53,7 @@ def majorana(idx): rtn._string_rep.string = 'χ[%d]' % idx rtn._string_rep.tex = r'\chi_{IDX%d}' % idx + rtn._string_rep.repr_str = f'majorana({idx})' rtn._string_rep.brackets = '' return rtn diff --git a/tests/unit/test_extras.py b/tests/unit/test_extras.py new file mode 100644 index 0000000..a1ea5f7 --- /dev/null +++ b/tests/unit/test_extras.py @@ -0,0 +1,51 @@ + +import unittest as ut + +from dynamite.operators import sigmax, sigmay, sigmaz, identity, zero +from dynamite.extras import commutator, majorana + + +class Commutator(ut.TestCase): + + def test_paulis(self): + self.assertEqual(commutator(sigmax(), sigmay()), 2j*sigmaz()) + self.assertEqual(commutator(sigmaz(), sigmay()), -2j*sigmax()) + + def test_str(self): + self.assertEqual(str(commutator(sigmax(1), sigmay(2))), '[σx[1], σy[2]]') + + def test_tex(self): + self.assertEqual(commutator(sigmax(1), sigmay(2))._repr_latex_(), '$\\left[ \\sigma^x_{1}, \\sigma^y_{2} \\right]$') + + def test_repr(self): + expr = 'commutator(sigmax(1), sigmay(2))' + self.assertEqual(repr(eval(expr)), expr) + + +class Majorana(ut.TestCase): + + def test_relations(self): + # majorana is its own antiparticle + self.assertEqual(majorana(0)*majorana(0), identity()) + self.assertEqual(majorana(10)*majorana(10), identity()) + + # majoranas anticommute with each other + self.assertEqual(majorana(0)*majorana(1) + majorana(1)*majorana(0), zero()) + self.assertEqual(majorana(0)*majorana(2) + majorana(2)*majorana(0), zero()) + self.assertEqual(majorana(10)*majorana(1) + majorana(1)*majorana(10), zero()) + + def test_str(self): + self.assertEqual(str(majorana(0)), 'χ[0]') + self.assertEqual(str(majorana(1)), 'χ[1]') + + def test_tex(self): + self.assertEqual(majorana(0)._repr_latex_(), '$\\chi_{0}$') + self.assertEqual(majorana(1)._repr_latex_(), '$\\chi_{1}$') + + def test_repr(self): + expr = 'majorana(1)' + self.assertEqual(repr(eval(expr)), expr) + + +if __name__ == '__main__': + ut.main() From 9ca958f623c269baf5cb2c14bc74ec9cae5604ec Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 15 Mar 2024 15:44:51 -1000 Subject: [PATCH 55/73] improve Operator.__str__ and Operator.table() formatting --- CHANGELOG.md | 1 + src/dynamite/msc_tools.py | 71 +++++++++++---- src/dynamite/operators.py | 11 ++- tests/unit/test_msc_tools.py | 164 +++++++++++++++++++++++------------ tests/unit/test_operators.py | 126 +++++++++++++++++++++++++++ 5 files changed, 291 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b4320c..ea5142c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Explicit subspace sometimes failed conservation check even when operator was actually conserved - Build was broken with Cython 3 - Work around broken `petsc4py` and `slepc4py` builds with `pip>=23.1` (see [PETSc issue](https://gitlab.com/petsc/petsc/-/issues/1369)) + - `Operator.__str__` and `Operator.table()` were formatted poorly for operators with complex coefficients ## 0.3.1 - 2023-03-07 diff --git a/src/dynamite/msc_tools.py b/src/dynamite/msc_tools.py index ba20ef4..6de17df 100644 --- a/src/dynamite/msc_tools.py +++ b/src/dynamite/msc_tools.py @@ -400,35 +400,68 @@ def table(msc, L): non-Hermitian matrices. ''' - rtn = ' coeff. | {pad}operator{pad} \n' +\ - '====================={epad}\n' - - npad = max(L - 8, 0) - rtn = rtn.format(pad = ' '*(npad//2), epad = '='*npad) - - terms = [] + coeff_strs = [] + pauli_strs = [] for m, s, c in msc: - if 1E-2 < abs(c) < 1E2: - term = ' {:8.3f} ' - else: - term = ' {:.2e} ' - - term += '| ' - + pauli_str = '' for i in range(L): maskbit = (m >> i) & 1 signbit = (s >> i) & 1 - term += [['-', 'Z'], - ['X', 'Y']][maskbit][signbit] + pauli_str += [['-', 'Z'], + ['X', 'Y']][maskbit][signbit] if maskbit and signbit: c *= -1j - term = term.format(c.real) - terms.append(term) + coeff_strs.append(_get_coeff_str(c, trunc=True)) + pauli_strs.append(pauli_str) + + coeff_just_len = max(7, max((len(s) for s in coeff_strs), default=0)) + + rtn = f' {"coeff.".center(coeff_just_len)}' + rtn += ' | ' + + npad_operator = max(L - 8, 0)//2 + text_pad = ' '*npad_operator + rtn += f'{text_pad}operator{text_pad} \n' + rtn += '='*(len(rtn)-1) + rtn += '\n' - rtn += '\n'.join(terms) + rtn += '\n'.join(f' {cstr.rjust(coeff_just_len)} | {pstr}' for cstr, pstr in zip(coeff_strs, pauli_strs)) + + return rtn + + +def _get_coeff_str(x, trunc=False, parens=False): + if trunc: + both_parts = x.real != 0 and x.imag != 0 + if both_parts: + if 1E-2 <= abs(x) <= 1E2 or x == 0: + rtn = f'{x:.2f}' + else: + rtn = f'{x:.2e}' + + else: + big = not (1E-2 <= abs(x) <= 1E2) and not x == 0 + if x.imag: + if big: + rtn = f'{x.imag:.2e}j' + else: + rtn = f'{x.imag:.3f}j' + else: + if big: + rtn = f'{x.real:.2e}' + else: + rtn = f'{x.real:.3f}' + + if parens and (both_parts or 'e' in rtn): + rtn = f'({rtn})' + + else: + rtn = str(x) + if not parens and '(' in rtn: + rtn = rtn[1:-1] return rtn diff --git a/src/dynamite/operators.py b/src/dynamite/operators.py index 0eb4610..eeea3a6 100644 --- a/src/dynamite/operators.py +++ b/src/dynamite/operators.py @@ -1128,12 +1128,11 @@ def scale(self, x): except (ValueError, TypeError): raise TypeError(f'Cannot scale operator by type {type(x)}') - # coefficient up to 3 digits of precision, with trailing zeros removed - coeff_str = f'{x:.3f}'.rstrip('0').rstrip('.') + coeff_str = msc_tools._get_coeff_str(x, parens=True) - self._string_rep.string = coeff_str + self._string_rep.with_brackets('string') + self._string_rep.string = f'{coeff_str}*{self._string_rep.with_brackets("string")}' self._string_rep.tex = coeff_str + self._string_rep.with_brackets('tex') - self._string_rep.repr_str = str(x) + '*' + self._string_rep.with_brackets('repr') + self._string_rep.repr_str = f'{coeff_str}*{self._string_rep.with_brackets("repr")}' self._string_rep.brackets = '' def _num_mul(self, x): @@ -1321,7 +1320,7 @@ def index_sum(op, size=None, start=0, boundary='open'): string_rep = _OperatorStringRep() - string_rep.string = 'index_sum(' + str(op) + ', sites %d - %d' % (start, stop-1) + string_rep.string = f'index_sum({op}, sites {start}-{stop-1}' string_rep.repr_str = f'index_sum({repr(op)}' if not default_size: string_rep.repr_str += f', size={size}' @@ -1386,7 +1385,7 @@ def index_product(op, size=None, start=0): stop = start + size - op.max_spin_idx string_rep = _OperatorStringRep( - string='index_product(' + str(op) + ', sites %d - %d)' % (start, stop-1) + string=f'index_product({op}, sites {start}-{stop-1})' ) string_rep.repr_str = f'index_product({repr(op)}' diff --git a/tests/unit/test_msc_tools.py b/tests/unit/test_msc_tools.py index d8195ed..ae36c3d 100644 --- a/tests/unit/test_msc_tools.py +++ b/tests/unit/test_msc_tools.py @@ -716,42 +716,35 @@ class Table(ut.TestCase): def test_empty_L5(self): L = 5 msc = [] - correct = ' coeff. | operator \n' +\ - '=====================\n' + correct = ' coeff. | operator \n' +\ + '====================\n' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) - def test_empty_L9(self): - L = 9 - msc = [] - correct = ' coeff. | operator \n' +\ - '======================\n' + def test_identity(self): + L = 5 + msc = [(0, 0, 2.3)] + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 2.300 | -----' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) - def test_empty_L10(self): + def test_identity_L10(self): L = 10 - msc = [] - correct = ' coeff. | operator \n' +\ - '=======================\n' - check = msc_tools.table(msc, L) - self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) - - def test_identity(self): - L = 5 msc = [(0, 0, 2.3)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 2.300 | -----' + correct = ' coeff. | operator \n' +\ + '======================\n' +\ + ' 2.300 | ----------' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_sigmax_0(self): L = 5 msc = [(1, 0, 1)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 1.000 | X----' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000 | X----' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) @@ -759,87 +752,87 @@ def test_sigmaz_0(self): L = 5 L = 5 msc = [(0, 1, 1)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 1.000 | Z----' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000 | Z----' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_sigmay_0(self): L = 5 msc = [(1, 1, 1j)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 1.000 | Y----' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000 | Y----' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_sigmay_2(self): L = 5 msc = [(4, 4, 1j)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 1.000 | --Y--' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000 | --Y--' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_sigmax_0_coeff(self): L = 5 msc = [(1, 0, 3.141592)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 3.142 | X----' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 3.142 | X----' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_XX(self): L = 5 msc = [(3, 0, 1)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 1.000 | XX---' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000 | XX---' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_ZZ(self): L = 5 msc = [(0, 3, 1)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 1.000 | ZZ---' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000 | ZZ---' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_XYZ(self): L = 5 msc = [(3, 6, 0.5j)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 0.500 | XYZ--' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 0.500 | XYZ--' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_three(self): L = 5 msc = [(1, 0, 2.3), (3, 1, 2j), (3, 6, 0.5j)] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 2.300 | X----\n' +\ - ' 2.000 | YX---\n' +\ - ' 0.500 | XYZ--' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 2.300 | X----\n' +\ + ' 2.000 | YX---\n' +\ + ' 0.500 | XYZ--' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) def test_ZZ_wrap(self): L = 5 msc = [(0, 3, 0.25), (0, 6, 0.25), (0, 12, 0.25), (0, 24, 0.25), (0, 17, 0.25),] - correct = ' coeff. | operator \n' +\ - '=====================\n' +\ - ' 0.250 | ZZ---\n' +\ - ' 0.250 | -ZZ--\n' +\ - ' 0.250 | --ZZ-\n' +\ - ' 0.250 | ---ZZ\n' +\ - ' 0.250 | Z---Z' + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 0.250 | ZZ---\n' +\ + ' 0.250 | -ZZ--\n' +\ + ' 0.250 | --ZZ-\n' +\ + ' 0.250 | ---ZZ\n' +\ + ' 0.250 | Z---Z' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) @@ -847,7 +840,7 @@ def test_ZZ_wrap(self): def test_large_coeff(self): L = 5 msc = [(0, 1, 1E+9)] - correct = ' coeff. | operator \n' +\ + correct = ' coeff. | operator \n' +\ '=====================\n' +\ ' 1.00e+09 | Z----' check = msc_tools.table(msc, L) @@ -856,11 +849,68 @@ def test_large_coeff(self): def test_small_coeff(self): L = 5 msc = [(0, 1, 1E-9)] - correct = ' coeff. | operator \n' +\ + correct = ' coeff. | operator \n' +\ '=====================\n' +\ ' 1.00e-09 | Z----' check = msc_tools.table(msc, L) self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + def test_long_coeff(self): + L = 5 + msc = [(0, 1, 1.23456789)] + # we want it to round + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.235 | Z----' + check = msc_tools.table(msc, L) + self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + + def test_zero_coeff(self): + L = 5 + msc = [(0, 1, 0)] + # we want it to round + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 0.000 | Z----' + check = msc_tools.table(msc, L) + self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + + def test_imag_int(self): + L = 5 + msc = [(0, 1, 1j)] + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.000j | Z----' + check = msc_tools.table(msc, L) + self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + + def test_mixed_int(self): + L = 5 + msc = [(0, 1, 1+1j)] + correct = ' coeff. | operator \n' +\ + '=======================\n' +\ + ' 1.00+1.00j | Z----' + check = msc_tools.table(msc, L) + self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + + def test_imag_float(self): + L = 5 + msc = [(0, 1, 1.2501j)] + correct = ' coeff. | operator \n' +\ + '====================\n' +\ + ' 1.250j | Z----' + check = msc_tools.table(msc, L) + self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + + def test_mixed_float(self): + L = 5 + msc = [(0, 1, 1.231+1.341j)] + correct = ' coeff. | operator \n' +\ + '=======================\n' +\ + ' 1.23+1.34j | Z----' + check = msc_tools.table(msc, L) + self.assertEqual(check, correct, msg = '\n' + '\n\n'.join([check, correct])) + + if __name__ == '__main__': ut.main() diff --git a/tests/unit/test_operators.py b/tests/unit/test_operators.py index 380b474..7fd5770 100644 --- a/tests/unit/test_operators.py +++ b/tests/unit/test_operators.py @@ -1036,6 +1036,132 @@ def test_misc(self): for case in test_cases: self.assertEqual(case, repr(eval(case))) + def test_constants(self): + test_cases = [ + '1.234*sigmaz(0)', + '1.234j*sigmaz(0)', + '1j*sigmaz(0)', + '(1+1j)*sigmaz(0)', + '(1.123+1.234j)*sigmaz(0)', + ] + for case in test_cases: + self.assertEqual(case, repr(eval(case))) + + +class Str(ut.TestCase): + ''' + Test the result of calling str() on operators. + ''' + + def test_paulis(self): + test_cases = [ + (sigmax(), 'σx[0]'), + (sigmay(), 'σy[0]'), + (sigmaz(), 'σz[0]'), + (sigma_plus(), 'σ+[0]'), + (sigma_minus(), 'σ-[0]'), + (sigmax(2), 'σx[2]'), + (sigmay(2), 'σy[2]'), + (sigmaz(2), 'σz[2]'), + (sigma_plus(2), 'σ+[2]'), + (sigma_minus(2), 'σ-[2]'), + (identity(), '1'), + (zero(), '0'), + ] + for op, str_val in test_cases: + self.assertEqual(str(op), str_val) + + def test_sums(self): + test_cases = [ + (sigmax(0) + sigmax(1), 'σx[0] + σx[1]'), + (sigmay(0) + sigmax(0), 'σy[0] + σx[0]'), + (sigmaz(1) + sigmax(2), 'σz[1] + σx[2]'), + (sigma_plus(0) + sigma_minus(0), 'σ+[0] + σ-[0]'), + ( + index_sum(sigmax(0), size=10), + 'index_sum(σx[0], sites 0-9)' + ), + ( + index_sum(sigmax(0), size=10, boundary="closed"), + 'index_sum(σx[0], sites 0-9, wrapped)' + ), + ( + index_sum(sigmax(0), size=10, start=1), + 'index_sum(σx[0], sites 1-10)' + ), + ] + for op, str_val in test_cases: + self.assertEqual(str(op), str_val) + + def test_products(self): + test_cases = [ + (sigmax(0)*sigmax(1), 'σx[0]*σx[1]'), + (sigmay(0)*sigmax(0), 'σy[0]*σx[0]'), + (sigmaz(1)*sigmax(2), 'σz[1]*σx[2]'), + (sigma_plus(0)*sigma_minus(0), 'σ+[0]*σ-[0]'), + ( + index_product(sigmax(0), size=10), + 'index_product(σx[0], sites 0-9)' + ), + ( + index_product(sigmax(0), size=10, start=1), + 'index_product(σx[0], sites 1-10)' + ), + ] + for op, str_val in test_cases: + self.assertEqual(str(op), str_val) + + def test_op_sum_product(self): + test_cases = [ + ( + op_sum(sigmax(i) for i in range(4)), + 'σx[0] + σx[1] + σx[2] + ...' + ), + ( + op_sum((sigmax(i) for i in range(4)), nshow=1), + 'σx[0] + ...' + ), + ( + op_product(sigmax(i) for i in range(4)), + 'σx[0]*σx[1]*σx[2]*σx[3]' + ), + ( + op_product(sigmax(i)+sigmay(i) for i in range(4)), + '(σx[0] + σy[0])*(σx[1] + σy[1])*(σx[2] + σy[2])*(σx[3] + σy[3])' + ) + ] + for op, str_val in test_cases: + self.assertEqual(str(op), str_val) + + def test_misc(self): + test_cases = [ + ( + sigmaz(0)*(sigmax(0) + sigmay(1)), + 'σz[0]*(σx[0] + σy[1])' + ), + ( + index_sum(sigmax(0) + sigmax(1), size=10), + 'index_sum(σx[0] + σx[1], sites 0-8)' + ), + ] + for op, str_val in test_cases: + self.assertEqual(str(op), str_val) + + def test_constants(self): + test_cases = [ + (1*sigmaz(0), 'σz[0]'), + (2*sigmaz(0), '2*σz[0]'), + (1.234*sigmaz(0), '1.234*σz[0]'), + (1.23456*sigmaz(0), '1.23456*σz[0]'), + (1.234j*sigmaz(0), '1.234j*σz[0]'), + (1j*sigmaz(0), '1j*σz[0]'), + ((1+1j)*sigmaz(0), '(1+1j)*σz[0]'), + ((1.123+1.234j)*sigmaz(0), '(1.123+1.234j)*σz[0]') + ] + for op, str_val in test_cases: + with self.subTest(op=op, correct=str_val): + self.assertEqual(str(op), str_val) + if __name__ == '__main__': ut.main() From b491e03fa32596de9212e3073cab8e68f9908862 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 08:08:41 -1000 Subject: [PATCH 56/73] update changelog following 7804553a7c0ba668d887cb9ff979cb13c2ccc149 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea5142c..3bb5e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Build was broken with Cython 3 - Work around broken `petsc4py` and `slepc4py` builds with `pip>=23.1` (see [PETSc issue](https://gitlab.com/petsc/petsc/-/issues/1369)) - `Operator.__str__` and `Operator.table()` were formatted poorly for operators with complex coefficients + - various issues in `dynamite.extras` ## 0.3.1 - 2023-03-07 From 49475c8aefe98812a17ef2870471da665f334d4a Mon Sep 17 00:00:00 2001 From: Julia Wei Date: Fri, 22 Mar 2024 14:16:36 -0400 Subject: [PATCH 57/73] Enable multigpu computation Co-authored-by: Greg Kahanamoku-Meyer --- CHANGELOG.md | 1 + docs/FAQ.rst | 48 ++++++++--- docs/containers.rst | 7 +- docs/index.rst | 4 +- src/dynamite/__init__.py | 21 +++-- src/dynamite/_backend/bcuda_template_2.cu | 80 +++++++++++++------ .../_backend/bcuda_template_2_private.h | 3 +- src/dynamite/_backend/shell_context.h | 2 + tests/integration/run_all_tests.py | 3 - tests/integration/test_matrices.py | 7 ++ 10 files changed, 126 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb5e7d..f89e95d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - `Operator.precompute_diagonal` flag allows user to tune whether the matrix diagonal should be precomputed and saved, for shell matrices - `State.entanglement_entropy` member function (a more convenient way of using `computations.entanglement_entropy`, which also remains) - `tools.get_memory_usage` which can measure memory usage on a total, per rank, or per node basis + - Multi-GPU parallelism via GPU-aware MPI ### Removed - `--track_memory` flag to `benchmark.py`---now memory usage is always reported by the benchmarking script diff --git a/docs/FAQ.rst b/docs/FAQ.rst index 5464932..317688a 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -9,19 +9,24 @@ FAQ - :ref:`L` - :ref:`nondeterm` - :ref:`petsc` +- :ref:`gpu-aware-mpi` .. _parallel: How do I run my code in parallel? --------------------------------- -One of dynamite's most important features is its ability to scale across many processors using MPI (or even GPUs!). For example, running with four MPI ranks (processes) is as easy as the following: +One of dynamite's most important features is its ability to scale across multiple processors using MPI (or even multiple GPUs!). For example, running with four MPI ranks (processes) is as easy as the following: .. code:: bash mpirun -n 4 python3 solve_all_the_things.py -Note that this should come at the end of the ``docker run...`` command if you are using the container images to run dynamite. See :ref:`containers` for details (and for information about how to run on GPUs). +Note that using MPI with containers can require special steps; see :ref:`containers` for details. + +To accelerate dynamite's computations using a GPU, the simplest way is to use one of the GPU-accelerated Docker images (again, see :ref:`containers` for details). +To parallelize across multiple GPUs requires building dynamite from source (see :ref:`installing`); dynamite should then be run with a number of MPI ranks equal to the number of available GPUs. +Achieving good performance with multiple GPUs requires a GPU-aware MPI library. .. _ranks: @@ -43,8 +48,8 @@ Here are a few ideas to explore for why your computation might be slow or not sc .. _shell: -**My computation is using too much memory.** --------------------------------------------- +My computation is using too much memory. +---------------------------------------- Even in the sparse form that dynamite uses by default, storing an operator's matrix can use large amounts of memory. To alleviate this problem, dynamite can be run with so-called "matrix-free" matrices (known in dynamite and PETSc as "shell" matrices). When this is enabled, matrix elements are computed on the fly instead of being stored explicitly, saving significantly on memory usage and sometimes even speeding things up. When using shell matrices, the memory usage is reduced essentially to the vectors used in the computations. @@ -52,8 +57,8 @@ Shell matrices can be enabled globally by setting ``dynamite.config.shell = True .. _integer: -**I got an error message about an integer overflow even though I'm running with fewer than 32 spins.** ------------------------------------------------------------------------------------------------------- +I got an error message about an integer overflow even though I'm running with fewer than 32 spins. +-------------------------------------------------------------------------------------------------- Even if the state vector length is shorter than :math:`2^{32}`, PETSc may allocate a block of many vectors at once, and the total length of this allocated block is greater than the maximum 32-bit integer. Before switching to 64-bit integers, try passing the ``-bv_type vecs`` flag to SLEPc by putting the following at the beginning of your script: @@ -66,8 +71,8 @@ That way each vector will be allocated individually. .. _L: -**I am tired of setting the spin chain length L everywhere.** -------------------------------------------------------------- +I am tired of setting the spin chain length L everywhere. +--------------------------------------------------------- There is an easy way to globally set a default value for ``L``. Before you start building any operators: @@ -82,15 +87,15 @@ for details. .. _nondeterm: -**My code is having mysterious problems/giving wrong answers when I run with more than 1 MPI rank.** -------------------------------------------------------------------------------------------------------- +My code is having mysterious problems/giving wrong answers when I run with more than 1 MPI rank. +------------------------------------------------------------------------------------------------ There are a number of reasons this could happen, but here is a likely culprit. Each MPI rank runs as an independent Python process, so non-deterministic code can behave differently across the ranks. For example, if you are iterating through an unordered data type like a Python dictionary or set, different ranks may iterate through the values in a different order! As another example, making calls to e.g. ``numpy.random.rand()`` will give different values on each process. If you use this when building your Hamiltonian, you will not have a consistent operator across your different processes! If you need random numbers, make sure to seed them with the same value everywhere. .. _petsc: -**I want to get under the hood and fiddle with PETSc and SLEPc.** ------------------------------------------------------------------ +I want to get under the hood and fiddle with PETSc and SLEPc. +------------------------------------------------------------- The underlying ``petsc4py`` matrix for any operator is accessible with :meth:`dynamite.operators.Operator.get_mat`. For states, the ``petsc4py`` vector @@ -116,3 +121,22 @@ one would do (although this particular case is built-in to dynamite, and can be accomplished via the ``ncv`` keyword argument to :meth:`dynamite.computations.evolve`). + +.. _gpu-aware-mpi: + +I am getting a warning from PETSc about not having GPU-aware MPI. +----------------------------------------------------------------- + +dynamite is designed to be able to run parallelized across multiple GPUs. For this to be performant, +it is crucial that the MPI implementation being used is GPU-aware, meaning that instead of transferring +data to the CPU, then to another processor via MPI, then to that processor's GPU, it can transfer data +directly between GPUs via e.g. NVLink. + +If you are running with multiple GPUs, the way to avoid this error is to ensure your MPI implementation +is GPU-aware---your performance will be quite bad otherwise. If you are compiling OpenMPI yourself, use +the ``--with-cuda`` flag to ``./configure``; if you are using a compute cluster's build of MPI, talk to +your system administrator. + +If you are running with a single GPU, MPI is simply not needed. In that case you can avoid the warning by +removing ``mpi4py`` from your Python environment, in which case dynamite will automatically disable +the warning, or by setting an environment variable as described in the PETSc error message. diff --git a/docs/containers.rst b/docs/containers.rst index 2bed8ed..85ace17 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -45,6 +45,7 @@ A quick explanation of the options: If you want to run with multiple processes using MPI, you can simply add ``mpirun -n `` before ``python`` in the command above. Note that on a cluster, to achieve the best MPI performance you should instead build from source (see :ref:`installing`) and use the cluster's native MPI. + Building from source is also currently the only way to run on multiple GPUs. Also, with Docker you may get errors unless you add the flag ``--cap-add=SYS_PTRACE``. .. _desktop_script: @@ -89,9 +90,9 @@ If you are on a node with an Nvidia GPU, running the CUDA-accelerated version of # or singularity shell --nv docker://gdmeyer/dynamite:latest-cuda # to start a shell with dynamite installed -The default version is compiled for GPUs with compute capability >= 7.0; there are images on DockerHub compiled -with other compute capabilities (e.g. ``docker://gdmeyer/dynamite:latest-cuda.cc80`` for compute capability 8.0). -You can see a list of all available images `on DockerHub `_. +Although dynamite supports multi-GPU computations via CUDA-aware MPI, ensuring compatability between MPI inside and outside the docker +images is very difficult; therefore, the GPU docker images currently only support computations on a single GPU. To run multi-GPU computations, +please build from source (see :ref:`installing`). .. note :: dynamite with CUDA requires Nvidia driver >= 450.80.02 diff --git a/docs/index.rst b/docs/index.rst index 75aa162..6662f93 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,9 +7,7 @@ dynamite: fast numerics for quantum spin chains =============================================== Welcome to **dynamite**, which provides a simple interface -to fast evolution of quantum dynamics and eigensolving. Behind the -scenes, dynamite uses the PETSc/SLEPc implementations of Krylov subspace -exponentiation and eigensolving. +to fast parallel evolution of quantum dynamics and eigensolving via Krylov subspace methods. Quick start ----------- diff --git a/src/dynamite/__init__.py b/src/dynamite/__init__.py index a89ec71..5fd7fd3 100644 --- a/src/dynamite/__init__.py +++ b/src/dynamite/__init__.py @@ -107,9 +107,16 @@ def _initialize(self, slepc_args=None, version_check=True, gpu=None): '-options_left', '0' ] - # prevent PETSc from being sad if we don't use gpu aware mpi - if not self.initialized and bbuild.have_gpu_shell(): - slepc_args += ['-use_gpu_aware_mpi', '0'] # we only use one process anyway + # use mpi4py as a slightly crude check for whether we need GPU-aware MPI + try: + import mpi4py + except ImportError: + mpi4py = None + + if mpi4py is None and bbuild.have_gpu_shell(): + # disables annoying error message in the case where we don't have + # GPU-aware MPI, but we are only running with one rank so we don't care + slepc_args += ['-use_gpu_aware_mpi', '0'] if bbuild.petsc_initialized(): raise RuntimeError('PETSc has been initialized but dynamite has not. ' @@ -123,11 +130,15 @@ def _initialize(self, slepc_args=None, version_check=True, gpu=None): from petsc4py import PETSc - # disable extra thread-level parallelism that can interfere with MPI - # parallelism if PETSc.COMM_WORLD.size != 1: + # disable extra thread-level parallelism that can interfere with MPI + # parallelism threadpool_limits(limits=1) + if mpi4py is None: + raise ImportError('could not import mpi4py, which is required when running with ' + 'multiple ranks') + if version_check and PETSc.COMM_WORLD.rank == 0: check_version() diff --git a/src/dynamite/_backend/bcuda_template_2.cu b/src/dynamite/_backend/bcuda_template_2.cu index 84e660e..a2a49aa 100644 --- a/src/dynamite/_backend/bcuda_template_2.cu +++ b/src/dynamite/_backend/bcuda_template_2.cu @@ -8,14 +8,10 @@ PetscErrorCode C(BuildGPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( int xparity, Mat *A) { - PetscInt M, N, mpi_size; + PetscInt M, N, m, n, mpi_size; shell_context *ctx; PetscCallMPI(MPI_Comm_size(PETSC_COMM_WORLD, &mpi_size)); - if (mpi_size > 1) { - SETERRQ(PETSC_COMM_WORLD, PETSC_ERR_SUP, - "Shell GPU matrices currently only implemented for 1 MPI process."); - } /* N is dimension of right subspace, M of left */ M = C(Dim,LEFT_SUBSPACE)(left_subspace_data); @@ -25,10 +21,15 @@ PetscErrorCode C(BuildGPUShell,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( N /= 2; } + m = PETSC_DECIDE; + n = PETSC_DECIDE; + PetscCall(PetscSplitOwnership(PETSC_COMM_WORLD, &m, &M)); + PetscCall(PetscSplitOwnership(PETSC_COMM_WORLD, &n, &N)); + PetscCall(C(BuildContext_CUDA,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( msc, left_subspace_data, right_subspace_data, &ctx)); - PetscCall(MatCreateShell(PETSC_COMM_WORLD, M, N, M, N, ctx, A)); + PetscCall(MatCreateShell(PETSC_COMM_WORLD, m, n, M, N, ctx, A)); PetscCall(MatShellSetOperation(*A, MATOP_MULT, (void(*)(void))C(MatMult_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE)))); @@ -60,6 +61,14 @@ PetscErrorCode C(BuildContext_CUDA,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( ctx->gpu = PETSC_TRUE; nterms = msc->mask_offsets[msc->nmasks]; + int mpi_rank, count; + MPI_Comm_rank(PETSC_COMM_WORLD, &mpi_rank); + cudaGetDeviceCount(&count); + cudaSetDevice(mpi_rank%count); + + // scatter context + ctx->sc_ctx = PETSC_NULLPTR; + PetscCallCUDA(cudaMalloc((void **) &(ctx->masks), sizeof(PetscInt)*msc->nmasks)); PetscCallCUDA(cudaMemcpy(ctx->masks, msc->masks, sizeof(PetscInt)*msc->nmasks, @@ -104,6 +113,12 @@ PetscErrorCode C(MatDestroyCtx_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A) PetscCall(MatShellGetContext(A, &ctx)); + if (ctx->sc_ctx != PETSC_NULLPTR) { + PetscCall(VecScatterDestroy(&(ctx->sc_ctx))); + PetscCall(VecDestroy(&(ctx->x_all))); + ctx->sc_ctx = PETSC_NULLPTR; + } + PetscCallCUDA(cudaFree(ctx->masks)); PetscCallCUDA(cudaFree(ctx->mask_offsets)); PetscCallCUDA(cudaFree(ctx->signs)); @@ -129,21 +144,35 @@ PetscErrorCode C(MatMult_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, Vec const PetscScalar* xarray; PetscScalar* barray; - PetscInt size; - - PetscCall(VecSet(b, 0)); + PetscInt row_start, row_end, mpi_size; PetscCall(MatShellGetContext(A, &ctx)); - PetscCall(VecCUDAGetArrayRead(x, &xarray)); + MPI_Comm_size(PETSC_COMM_WORLD, &mpi_size); + + PetscCall(VecSet(b, 0)); PetscCall(VecCUDAGetArray(b, &barray)); + PetscCall(VecGetOwnershipRange(b, &row_start, &row_end)); - PetscCall(VecGetSize(b, &size)); + if (mpi_size == 1) { + PetscCall(VecCUDAGetArrayRead(x, &xarray)); + PetscCallCUDA(cudaDeviceSynchronize()); + } + else { + /* Scatter x to a sequential array */ + // Only do on the first multiplication + if (ctx->sc_ctx == PETSC_NULLPTR){ + PetscCall(VecScatterCreateToAll(x, &(ctx->sc_ctx), &(ctx->x_all))); + } + PetscCall(VecScatterBegin(ctx->sc_ctx, x, ctx->x_all, INSERT_VALUES, SCATTER_FORWARD)); + PetscCall(VecScatterEnd(ctx->sc_ctx, x, ctx->x_all, INSERT_VALUES, SCATTER_FORWARD)); - PetscCallCUDA(cudaDeviceSynchronize()); + PetscCall(VecCUDAGetArrayRead(ctx->x_all, &xarray)); + } C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))<<>>( - size, + row_start, + row_end, ctx->masks, ctx->mask_offsets, ctx->signs, @@ -153,18 +182,24 @@ PetscErrorCode C(MatMult_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, Vec (C(data,RIGHT_SUBSPACE)*) ctx->right_subspace_data, ctx->diag, xarray, - barray); + barray + ); + + if (mpi_size == 1) { + PetscCall(VecCUDARestoreArrayRead(x, &xarray)); + } else { + PetscCall(VecCUDARestoreArrayRead(ctx->x_all, &xarray)); + } PetscCallCUDA(cudaDeviceSynchronize()); - PetscCall(VecCUDARestoreArrayRead(x, &xarray)); PetscCall(VecCUDARestoreArray(b, &barray)); - return 0; } __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( - PetscInt size, + PetscInt row_start, + PetscInt row_end, PetscInt* masks, PetscInt* mask_offsets, PetscInt* signs, @@ -176,12 +211,12 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( const PetscScalar* xarray, PetscScalar* barray) { - /* the following four lines come from the PETSc cuda source */ - PetscInt entries_per_group = (size - 1) / gridDim.x + 1; + PetscInt local_size = row_end - row_start; + PetscInt entries_per_group = (local_size - 1) / gridDim.x + 1; entries_per_group = (entries_per_group == 0) ? 1 : entries_per_group; // for very small vectors, a group should still do some work PetscInt vec_start_index = blockIdx.x * entries_per_group; - PetscInt vec_stop_index = PetscMin((blockIdx.x + 1) * entries_per_group, size); // don't go beyond vec size + PetscInt vec_stop_index = PetscMin((blockIdx.x + 1) * entries_per_group, local_size); // don't go beyond vec size PetscScalar tmp, val; PetscReal sign; @@ -190,10 +225,10 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( this_start = vec_start_index + threadIdx.x; for (row_idx = this_start; row_idx < vec_stop_index; row_idx += blockDim.x) { - ket = C(I2S_CUDA,LEFT_SUBSPACE)(row_idx,left_subspace_data); + ket = C(I2S_CUDA,LEFT_SUBSPACE)(row_idx + row_start, left_subspace_data); if (diag) { - val = diag[row_idx] * xarray[row_idx]; + val = diag[row_idx + row_start] * xarray[row_idx + row_start]; mask_idx = 1; } else { val = 0; @@ -234,7 +269,6 @@ __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( } barray[row_idx] = val; - } } diff --git a/src/dynamite/_backend/bcuda_template_2_private.h b/src/dynamite/_backend/bcuda_template_2_private.h index 846be3a..dd1a919 100644 --- a/src/dynamite/_backend/bcuda_template_2_private.h +++ b/src/dynamite/_backend/bcuda_template_2_private.h @@ -15,7 +15,8 @@ PetscErrorCode C(MatDestroyCtx_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A); PetscErrorCode C(MatMult_GPU,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))(Mat A, Vec x, Vec b); __global__ void C(device_MatMult,C(LEFT_SUBSPACE,RIGHT_SUBSPACE))( - PetscInt size, + PetscInt row_start, + PetscInt row_end, PetscInt* masks, PetscInt* mask_offsets, PetscInt* signs, diff --git a/src/dynamite/_backend/shell_context.h b/src/dynamite/_backend/shell_context.h index 4ef6d8e..831d237 100644 --- a/src/dynamite/_backend/shell_context.h +++ b/src/dynamite/_backend/shell_context.h @@ -22,4 +22,6 @@ typedef struct _shell_context { void *right_subspace_data; PetscReal nrm; PetscBool gpu; + VecScatter sc_ctx; // context for scattering + Vec x_all; // sequential x array } shell_context; diff --git a/tests/integration/run_all_tests.py b/tests/integration/run_all_tests.py index 157dd34..6fcfc2e 100644 --- a/tests/integration/run_all_tests.py +++ b/tests/integration/run_all_tests.py @@ -106,9 +106,6 @@ def main(): else: test_names = get_test_list(params.test_set) - if params.gpu: - params.mpiexec = '' - if params.nprocs is None: if not params.mpiexec: params.nprocs = [1] diff --git a/tests/integration/test_matrices.py b/tests/integration/test_matrices.py index 662c0db..945aa79 100644 --- a/tests/integration/test_matrices.py +++ b/tests/integration/test_matrices.py @@ -72,6 +72,13 @@ def petsc_mat_columns(mat): class Fundamental(dtr.DynamiteTestCase): def setUp(self): + config._initialize() + from petsc4py import PETSc + self.mpi_size = PETSc.COMM_WORLD.size + + if self.mpi_size > 2: + self.skipTest(f'number of ranks exceeds Hilbert space dimension') + self.old_L = config.L config._L = None From 38b044056f8876e9c87118a5f78bde9ece19ce6b Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 09:35:30 -1000 Subject: [PATCH 58/73] bump PETSc/SLEPc version numbers --- docker/Dockerfile | 4 ++-- docs/install.rst | 4 ++-- pyproject.toml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 68b38e0..f376d82 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,8 +1,8 @@ ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' -ARG PETSC_VERSION=3.20.4 -ARG SLEPC_VERSION=3.20.1 +ARG PETSC_VERSION=3.20.5 +ARG SLEPC_VERSION=3.20.2 ARG CUDA_VERSION=12.2.2 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 diff --git a/docs/install.rst b/docs/install.rst index 2ab8e62..a715c8b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -36,7 +36,7 @@ following. There is a configuration script that comes with dynamite which should .. code:: bash - git clone --depth 1 --branch v3.20.4 https://gitlab.com/petsc/petsc.git petsc + git clone --depth 1 --branch v3.20.5 https://gitlab.com/petsc/petsc.git petsc cd petsc python /petsc_config/complex-opt.py @@ -61,7 +61,7 @@ Now download and install SLEPc: .. code:: bash - git clone --depth 1 --branch v3.20.1 https://gitlab.com/slepc/slepc.git slepc + git clone --depth 1 --branch v3.20.2 https://gitlab.com/slepc/slepc.git slepc cd slepc ./configure diff --git a/pyproject.toml b/pyproject.toml index 2cef16c..dbbc2e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ "numpy", "scipy", "threadpoolctl", - "petsc4py == 3.20.4", - "slepc4py == 3.20.1", + "petsc4py == 3.20.5", + "slepc4py == 3.20.2", ] [build-system] From 3b512a8ec9db1540017e548a4cbf6561ede5151b Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Wed, 11 Oct 2023 12:23:25 -1000 Subject: [PATCH 59/73] fix docker command in docs homepage --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 6662f93..f0adb70 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,7 +17,7 @@ To run the tutorial, `install Docker `_ .. code:: - docker run --rm -p 8887:8887 -w /home/dnm/examples/tutorial gdmeyer/dynamite:latest + docker run --rm -p 8887:8887 -w /home/dnm/examples/tutorial gdmeyer/dynamite:latest-jupyter Then follow the last link in the output (it should start with ``http://127.0.0.1:8887``). Start the tutorial by launching the notebook ``0-Welcome.ipynb`` in the left panel. From 4f18af191c7943a0ee108749e11280146fe9df1b Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 10:09:10 -1000 Subject: [PATCH 60/73] small updates to docs --- docs/FAQ.rst | 2 +- docs/containers.rst | 5 ++--- docs/install.rst | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/FAQ.rst b/docs/FAQ.rst index 317688a..af7b065 100644 --- a/docs/FAQ.rst +++ b/docs/FAQ.rst @@ -134,7 +134,7 @@ directly between GPUs via e.g. NVLink. If you are running with multiple GPUs, the way to avoid this error is to ensure your MPI implementation is GPU-aware---your performance will be quite bad otherwise. If you are compiling OpenMPI yourself, use -the ``--with-cuda`` flag to ``./configure``; if you are using a compute cluster's build of MPI, talk to +the ``--with-cuda`` flag to OpenMPI's ``./configure``; if you are using a compute cluster's build of MPI, talk to your system administrator. If you are running with a single GPU, MPI is simply not needed. In that case you can avoid the warning by diff --git a/docs/containers.rst b/docs/containers.rst index 85ace17..25ebbd8 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -7,9 +7,6 @@ Running dynamite from a container These instructions describe how to run ``dynamite`` from a pre-built container image. If you wish to instead install ``dynamite`` directly, see :ref:`installing`. -.. note:: - Running from containers is currently experimental. Please let us know if you run into any issues or have any suggestions! - On a personal computer, it's easiest to run the container using `podman `_ or `Docker `_. On a shared computer cluster, one can use `Singularity `_, which should come pre-installed in most HPC settings (see your cluster's documentation). @@ -141,8 +138,10 @@ It may take some tweaking for your specific compute cluster, but the basic steps 1. Login, and allocate a compute node for yourself on the cluster (e.g. with ``salloc`` in SLURM). 2. In a separate terminal, tunnel port 8887 to your local machine through ssh: + - Run ``ssh -NL 8887::8887 @`` - The above command should not generate any output + 3. On the compute node from Step 1, run ``singularity run docker://gdmeyer/dynamite:latest-jupyter`` 4. Follow the last link in the output (the one with ``127.0.0.1``) diff --git a/docs/install.rst b/docs/install.rst index a715c8b..6940c70 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -4,11 +4,10 @@ Installing from source ********************** -.. - The easiest way to use ``dynamite`` is through the pre-built container images---see :ref:`containers`. - If for some reason you can't use the containers, or if you want a site-specific build (for example to optimize message passing performance between nodes on a cluster), you can build from source. +.. note:: -The following instructions allow one to build ``dynamite`` manually from source. Experimental support has also been added to use dynamite from a pre-built Docker container; see :ref:`containers` for instructions. + The easiest way to use ``dynamite`` is through the pre-built container images---see :ref:`containers`. + If for some reason you can't or do not want to use the containers, or if you want a site-specific build (for example to optimize message passing performance between nodes on a cluster), you can build from source using the following instructions. Building from source ==================== From d58cdaf5a7d546ef5c558f143c582a54daa4e621 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 10:09:50 -1000 Subject: [PATCH 61/73] update docs dependencies --- docs/rtd_requirements.txt | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/rtd_requirements.txt b/docs/rtd_requirements.txt index 30d2473..9ac6acc 100644 --- a/docs/rtd_requirements.txt +++ b/docs/rtd_requirements.txt @@ -1,28 +1,28 @@ -alabaster==0.7.13 -Babel==2.13.1 -certifi==2023.7.22 +alabaster==0.7.16 +Babel==2.14.0 +certifi==2024.2.2 charset-normalizer==3.3.2 -docutils==0.18.1 -idna==3.4 +docutils==0.20.1 +idna==3.6 imagesize==1.4.1 -Jinja2==3.1.2 +Jinja2==3.1.3 markdown-it-py==3.0.0 -MarkupSafe==2.1.3 +MarkupSafe==2.1.5 mdit-py-plugins==0.4.0 mdurl==0.1.2 myst-parser==2.0.0 -packaging==23.2 -Pygments==2.16.1 +packaging==24.0 +Pygments==2.17.2 PyYAML==6.0.1 requests==2.31.0 snowballstemmer==2.2.0 Sphinx==7.2.6 -sphinx-rtd-theme==1.3.0 -sphinxcontrib-applehelp==1.0.7 -sphinxcontrib-devhelp==1.0.5 -sphinxcontrib-htmlhelp==2.0.4 +sphinx-rtd-theme==2.0.0 +sphinxcontrib-applehelp==1.0.8 +sphinxcontrib-devhelp==1.0.6 +sphinxcontrib-htmlhelp==2.0.5 sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 -sphinxcontrib-qthelp==1.0.6 -sphinxcontrib-serializinghtml==1.1.9 -urllib3==2.0.7 +sphinxcontrib-qthelp==1.0.7 +sphinxcontrib-serializinghtml==1.1.10 +urllib3==2.2.1 From 31f1a3b99dc8bbd80e4f312c716a6fd03639360b Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 15:49:14 -1000 Subject: [PATCH 62/73] explicitly disable shift-invert on GPU --- CHANGELOG.md | 1 + src/dynamite/computations.py | 7 ++++++- tests/integration/test_eigsolve.py | 14 +++++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89e95d..e52f1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - GPU builds now automatically switch to CPU if a GPU is not found (and print a warning) - Changed default bind mount location for Docker images to the container user's home directory, `/home/dnm` - Renamed some values of `which` argument of `eigsolve()`: `smallest`→`lowest` and `largest`→`highest` + - Shift-invert ("target") eigsolving on GPU disabled, as PETSc does not support it well ### Fixed - Explicit subspace sometimes failed conservation check even when operator was actually conserved diff --git a/src/dynamite/computations.py b/src/dynamite/computations.py index 7722fe9..bf08e78 100644 --- a/src/dynamite/computations.py +++ b/src/dynamite/computations.py @@ -212,7 +212,12 @@ def eigsolve(H, getvecs=False, nev=1, which='lowest', target=None, tol=None, sub which = 'target' if H.shell: - raise TypeError('Shift-invert ("target") not supported for shell matrices.') + raise RuntimeError('Shift-invert ("target") not supported for shell matrices.') + + if config.gpu: + raise RuntimeError('GPU-accelerated shift-invert ("target") eigensolving is not ' + 'currently well-supported by PETSc, and is thus currently ' + 'unavailable in dynamite.') st = eps.getST() st.setType(SLEPc.ST.Type.SINVERT) diff --git a/tests/integration/test_eigsolve.py b/tests/integration/test_eigsolve.py index b9217fb..fdf8cf6 100644 --- a/tests/integration/test_eigsolve.py +++ b/tests/integration/test_eigsolve.py @@ -137,8 +137,8 @@ def test_all_lowest(self): self.check_all(H, evals, evecs, tol=1E-12, evec_tol=1E-11) def test_all_target(self): - if config.shell: - self.skipTest("solving for target not supported with shell matrices") + if config.shell or config.gpu: + self.skipTest("solving for target not supported with shell or GPU matrices") for H_name in hamiltonians.get_names(complex_enabled()): with self.subTest(H=H_name): @@ -163,17 +163,21 @@ def test_lowest(self): self.check_all(H, evals, evecs, tol=1E-11, evec_tol=1E-9) def test_target(self): - if config.shell: - self.skipTest("solving for target not supported with shell matrices") - # coefficients that aren't commensurate but also not random for # repeatability H = op_sum(np.sin(i)*sigmax(i) for i in range(config.L)) + + if config.shell or config.gpu: + with self.assertRaises(RuntimeError): + H.eigsolve(target=0) + return + for target in [0.011, 0.999]: with self.subTest(target=target): evals, evecs = H.eigsolve(nev=5, getvecs=True, tol=1E-12, target=target) self.check_all(H, evals, evecs, tol=1E-11, evec_tol=1E-9) + class Subspaces(Checker): def test_all_subspaces(self): From 951fe2a53b7387362fef42480f226692e9eca85f Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 16:02:05 -1000 Subject: [PATCH 63/73] do not use deprecated Cython directives --- setup.py | 8 ++++---- src/dynamite/_backend/bpetsc.pyx | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 3621d46..88010b0 100644 --- a/setup.py +++ b/setup.py @@ -62,10 +62,10 @@ def write_build_headers(): 'src/dynamite/_backend/config.pxi' ) with open(header_path, 'w') as f: - f.write('DEF USE_CUDA = %d\n' % int(check_cuda())) - f.write('DEF DNM_BRANCH = "%s"\n' % branch) - f.write('DEF DNM_COMMIT = "%s"\n' % commit) - f.write('DEF DNM_VERSION = "%s"\n' % version) + f.write('USE_CUDA = %d\n' % int(check_cuda())) + f.write('DNM_BRANCH = "%s"\n' % branch) + f.write('DNM_COMMIT = "%s"\n' % commit) + f.write('DNM_VERSION = "%s"\n' % version) def extensions(): diff --git a/src/dynamite/_backend/bpetsc.pyx b/src/dynamite/_backend/bpetsc.pyx index a4d9861..90d2e3f 100644 --- a/src/dynamite/_backend/bpetsc.pyx +++ b/src/dynamite/_backend/bpetsc.pyx @@ -30,6 +30,7 @@ cdef extern from "bpetsc_impl.h": ctypedef bint PetscBool int DNM_PETSC_COMPLEX + int DNM_PETSC_CUDA ctypedef enum shell_impl: NO_SHELL @@ -117,7 +118,7 @@ def build_mat(PetscInt [:] masks, bsubspace.set_data_pointer(subspaces.right_type, right_subspace['data'], &(subspaces.right_data)) if gpu: - IF not USE_CUDA: + if not DNM_PETSC_CUDA: raise RuntimeError("dynamite was not built with CUDA shell " "functionality (requires nvcc during build).") From ac681be81f37fc2e9a9c4aadd52598fbe79865a5 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 16:05:32 -1000 Subject: [PATCH 64/73] bump CUDA version for docker --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index f376d82..e66b510 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,7 +3,7 @@ ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' ARG PETSC_VERSION=3.20.5 ARG SLEPC_VERSION=3.20.2 -ARG CUDA_VERSION=12.2.2 +ARG CUDA_VERSION=12.3.2 ARG CUDA_UBUNTU_VERSION=22.04 ARG CPU_PYTHON_VERSION=3.11 ARG SCALAR_TYPE=complex From 9e7035a3524b3e85140a22358e0bf6a89cb40a77 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 22 Mar 2024 19:30:49 -1000 Subject: [PATCH 65/73] fix version check --- src/dynamite/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dynamite/__init__.py b/src/dynamite/__init__.py index 5fd7fd3..309e8dc 100644 --- a/src/dynamite/__init__.py +++ b/src/dynamite/__init__.py @@ -257,7 +257,7 @@ def check_version(): url = 'https://raw.githubusercontent.com/GregDMeyer/dynamite/master/VERSION' try: with request.urlopen(url, timeout=1) as url_req: - release_version = url_req.read().strip() + release_version = url_req.read().strip().decode('UTF-8') except: return From cc160086cc1ab1c7f87ca15e935458fbc6303a19 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Sat, 23 Mar 2024 06:48:50 -1000 Subject: [PATCH 66/73] fix error in 64-bit test --- tests/integration/test_operators.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/integration/test_operators.py b/tests/integration/test_operators.py index afc98a0..a7420d4 100644 --- a/tests/integration/test_operators.py +++ b/tests/integration/test_operators.py @@ -190,6 +190,12 @@ def test_full_to_others(self): class SaveLoad(dtr.DynamiteTestCase): + def setUp(self): + self.old_L = config.L + + def tearDown(self): + config.L = self.old_L + def test_save_load(self): for H_name in hamiltonians.get_names(complex_enabled()): if 'slow' in self.skip_flags and H_name == 'syk': @@ -208,6 +214,7 @@ def test_load_int64_fail(self): have_int64 = msc_dtype['masks'].itemsize == 8 if have_int64: + config._L = None self.assertEqual( Operator.from_bytes(test_string), sigmax(33) From 4918aa5274e4279c324551128c27e91cdb16e75f Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Sat, 23 Mar 2024 06:49:01 -1000 Subject: [PATCH 67/73] fix hanging test --- tests/integration/test_evolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_evolve.py b/tests/integration/test_evolve.py index bab66ef..d36316e 100644 --- a/tests/integration/test_evolve.py +++ b/tests/integration/test_evolve.py @@ -48,11 +48,11 @@ def evolve_check(self, H, t, **kwargs): self.assertLess(np.abs(1 - bra.norm()), 1E-9) bra_check = bra.to_numpy() + norm = bra.norm() if ket_np is not None: bra_np = linalg.expm_multiply(-1j*t*H_np, ket_np) inner_prod = bra_check.dot(bra_np.conj()) - norm = bra.norm() self.assertLess(np.abs(1 - (inner_prod/(norm**2))), 1E-9, msg=f'inner prod:{inner_prod}; norm^2:{norm**2}') From 58503ace52ee3ec0ce02e5020fd4c5d80f5e50a5 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Sat, 23 Mar 2024 06:53:26 -1000 Subject: [PATCH 68/73] include LICENSE and README in docker builds --- .dockerignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 53388de..8279eb9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,8 +2,6 @@ docker/ docs/ .gitignore -LICENSE.txt -README.md # other files that might be generated build/ From 83261b9d31fe007d90c2af21afdebcf93d0d302c Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 29 Mar 2024 08:51:54 -1000 Subject: [PATCH 69/73] small updates to tutorial --- examples/tutorial/0-Welcome.ipynb | 2 +- examples/tutorial/1-Operators.ipynb | 2 +- examples/tutorial/2-States.ipynb | 2 +- examples/tutorial/3-Eigensolving.ipynb | 2 +- examples/tutorial/4-TimeEvolution.ipynb | 4 ++-- examples/tutorial/5-Subspaces.ipynb | 11 +++++++---- examples/tutorial/6-ShellMatrices.ipynb | 2 +- examples/tutorial/7-Conclusion.ipynb | 2 +- 8 files changed, 15 insertions(+), 12 deletions(-) diff --git a/examples/tutorial/0-Welcome.ipynb b/examples/tutorial/0-Welcome.ipynb index 5faed7b..ec34973 100644 --- a/examples/tutorial/0-Welcome.ipynb +++ b/examples/tutorial/0-Welcome.ipynb @@ -35,7 +35,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/1-Operators.ipynb b/examples/tutorial/1-Operators.ipynb index e3ba045..2b207dd 100644 --- a/examples/tutorial/1-Operators.ipynb +++ b/examples/tutorial/1-Operators.ipynb @@ -485,7 +485,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/2-States.ipynb b/examples/tutorial/2-States.ipynb index cfe8d33..c2933ba 100644 --- a/examples/tutorial/2-States.ipynb +++ b/examples/tutorial/2-States.ipynb @@ -289,7 +289,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/3-Eigensolving.ipynb b/examples/tutorial/3-Eigensolving.ipynb index 2bbb1b4..34f965a 100644 --- a/examples/tutorial/3-Eigensolving.ipynb +++ b/examples/tutorial/3-Eigensolving.ipynb @@ -347,7 +347,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/4-TimeEvolution.ipynb b/examples/tutorial/4-TimeEvolution.ipynb index 027046c..494fb33 100644 --- a/examples/tutorial/4-TimeEvolution.ipynb +++ b/examples/tutorial/4-TimeEvolution.ipynb @@ -194,7 +194,7 @@ "id": "71d6615a-3e21-4b04-abe4-5f83eb7de38c", "metadata": {}, "source": [ - "It's kind of beautiful!\n", + "It's kind of beautiful! If you'd like, try similarly plotting the evolution of the half-chain entanglement entropy over time. You can compute the entanglement entropy of a state via the `State.entanglement_entropy` function (see the documentation [here](https://dynamite.readthedocs.io/en/latest/dynamite.states.html#dynamite.states.State.entanglement_entropy)).\n", "\n", "This kind of evolution can be easily adapted to piecewise time-dependent Hamiltonians, by just adjusting the Hamiltonian that performs the evolution. For example, try implementing a Floquet evolution: try adding to the Heisenberg model a z-field that flips polarity with every period of T=1. This is most easily achieved by building two Hamiltonians (for each piece of the volution) and switching back and forth which one you use for the evolution step." ] @@ -236,7 +236,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/5-Subspaces.ipynb b/examples/tutorial/5-Subspaces.ipynb index 71b077c..753bc31 100644 --- a/examples/tutorial/5-Subspaces.ipynb +++ b/examples/tutorial/5-Subspaces.ipynb @@ -192,9 +192,12 @@ }, "outputs": [], "source": [ - "# this causes an error\n", - "#psi = State(state='U'*11 + 'D'*13, # first 11 spins up\n", - "# subspace=SpinConserve(L=config.L, k=config.L//2))" + "try:\n", + " # this causes an error\n", + " psi = State(state='U'*11 + 'D'*13, # first 11 spins up\n", + " subspace=SpinConserve(L=config.L, k=config.L//2))\n", + "except ValueError as e:\n", + " print(e)" ] }, { @@ -495,7 +498,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/6-ShellMatrices.ipynb b/examples/tutorial/6-ShellMatrices.ipynb index 2fcce9a..dcae15b 100644 --- a/examples/tutorial/6-ShellMatrices.ipynb +++ b/examples/tutorial/6-ShellMatrices.ipynb @@ -221,7 +221,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.1" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/tutorial/7-Conclusion.ipynb b/examples/tutorial/7-Conclusion.ipynb index 6048009..4f45cef 100644 --- a/examples/tutorial/7-Conclusion.ipynb +++ b/examples/tutorial/7-Conclusion.ipynb @@ -31,7 +31,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.8" } }, "nbformat": 4, From 54367d91b2665c12f44e2c2d4510a6f2dbcc305f Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 29 Mar 2024 10:34:39 -1000 Subject: [PATCH 70/73] improve docker build caching --- docker/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e66b510..2a3f9ee 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,3 +1,5 @@ +# syntax=docker.io/docker/dockerfile:1.7-labs +# ^ for --exclude flag to COPY. can remove if it gets mainlined ARG PLATFORM=cpu ARG BUILD_TYPE=opt # 'opt' or 'debug' @@ -134,7 +136,7 @@ RUN pip3 install --no-cache-dir $SLEPC_DIR/src/binding/slepc4py RUN mkdir /home/dnm/dynamite WORKDIR /home/dnm/dynamite -COPY --chown=dnm:dnm . . +COPY --chown=dnm:dnm --exclude=benchmarking/ --exclude=examples/ . . RUN pip3 install -v ./ From b6236614b711f2a0f2496f8614adfc6fd859c481 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 29 Mar 2024 10:35:05 -1000 Subject: [PATCH 71/73] misc improvements to example scripts --- examples/scripts/MBL/run_mbl.py | 5 +++-- examples/scripts/README.md | 2 +- examples/scripts/SYK/run_syk.py | 3 ++- examples/scripts/floquet/run_floquet.py | 22 +++++++++++++--------- examples/scripts/kagome/README.ipynb | 2 +- examples/scripts/kagome/README.md | 2 +- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/examples/scripts/MBL/run_mbl.py b/examples/scripts/MBL/run_mbl.py index 6156941..4245e61 100644 --- a/examples/scripts/MBL/run_mbl.py +++ b/examples/scripts/MBL/run_mbl.py @@ -52,7 +52,7 @@ def main(): min_eval = evals[0] # now the highest ones - evals, evecs = H.eigsolve(nev=args.nev, which='largest', getvecs=True) + evals, evecs = H.eigsolve(nev=args.nev, which='highest', getvecs=True) print_eig_stats(evals, evecs, h, energy_point=1) max_eval = evals[0] @@ -140,7 +140,8 @@ def parse_args(): parser = ArgumentParser() parser.add_argument('-L', type=int, required=True, help='spin chain length') - parser.add_argument('--seed', type=int, + # the weird type here allows passing integers in both decimal and hex + parser.add_argument('--seed', type=lambda x: int(x, 0), help='seed for random number generator. if omitted, a random ' 'seed is chosen by querying system hardware randomness') parser.add_argument('--iters', type=int, default=16, diff --git a/examples/scripts/README.md b/examples/scripts/README.md index deb3b0e..1f98b7d 100644 --- a/examples/scripts/README.md +++ b/examples/scripts/README.md @@ -29,7 +29,7 @@ Below we give (non-exhaustive) lists of the computations and programming pattern - Eigensolving for ground states - The `SpinConserve` subspace - The `XParity` subspace - - Computing correlation functions + - Computing correlation functions (TODO) ### SYK diff --git a/examples/scripts/SYK/run_syk.py b/examples/scripts/SYK/run_syk.py index 673a8cc..9383ef4 100644 --- a/examples/scripts/SYK/run_syk.py +++ b/examples/scripts/SYK/run_syk.py @@ -254,7 +254,8 @@ def parse_args(): parser.add_argument('--state-iters', default=1, type=int, help='number of random states per Hamiltonian') - parser.add_argument('-s', '--seed', type=int, + # the weird type here allows passing integers in both decimal and hex + parser.add_argument('-s', '--seed', type=lambda x: int(x, 0), help='seed for random number generator. if omitted, a random ' 'seed is chosen by querying system hardware randomness') diff --git a/examples/scripts/floquet/run_floquet.py b/examples/scripts/floquet/run_floquet.py index 76dfc50..8340811 100644 --- a/examples/scripts/floquet/run_floquet.py +++ b/examples/scripts/floquet/run_floquet.py @@ -9,12 +9,11 @@ from dynamite.operators import sigmax, sigmay, sigmaz, index_sum, index_product, op_sum from dynamite.states import State from dynamite.computations import entanglement_entropy -from dynamite.tools import mpi_print +from dynamite.tools import mpi_print, MPI_COMM_WORLD # TODO: what if it is killed while checkpointing? # (seems unlikely but could happen...) -# TODO: add shell flag def main(): @@ -27,6 +26,7 @@ def main(): mpi_print(file=stderr) # an extra newline config.L = args.L + config.shell = args.shell if args.checkpoint_every != 0: cycle_start, state = load_checkpoint(args.checkpoint_path) @@ -63,14 +63,16 @@ def main(): print_stats(state, cycle*args.T, tmp, Deff, Sz_ops) if args.checkpoint_every != 0 and cycle % args.checkpoint_every == 0: - # remove previous checkpoint - if cycle > args.checkpoint_every: - prev_cycle = cycle-args.checkpoint_every - to_remove = glob(join(args.checkpoint_path, f'floquet_cycle_{prev_cycle}*')) - for fname in to_remove: - remove(fname) state.save(join(args.checkpoint_path, f'floquet_cycle_{cycle}')) + # remove previous checkpoint, now that we have the new one + if MPI_COMM_WORLD().rank == 0: + if cycle > args.checkpoint_every: + prev_cycle = cycle-args.checkpoint_every + to_remove = glob(join(args.checkpoint_path, f'floquet_cycle_{prev_cycle}*')) + for fname in to_remove: + remove(fname) + def build_hamiltonian(alpha, Jz, Jx, h): # sums over all ranges of interaction @@ -171,7 +173,7 @@ def parse_args(): parser.add_argument('-T', type=float, default=0.12, help='Floquet period') - parser.add_argument('--initial-state-dwalls', default=1, + parser.add_argument('--initial-state-dwalls', type=int, default=1, help='Number of domain walls to include in initial product state') parser.add_argument('--n-cycles', type=int, default=int(1e4), @@ -184,6 +186,8 @@ def parse_args(): help='how frequently to save checkpoints, in number of cycles. ' 'if this option is omitted, checkpoints will not be saved.') + parser.add_argument('--shell', action='store_true', help='use matrix-free computation') + args = parser.parse_args() if len(args.h_vec) != 3: diff --git a/examples/scripts/kagome/README.ipynb b/examples/scripts/kagome/README.ipynb index 8813087..dcaa5f2 100644 --- a/examples/scripts/kagome/README.ipynb +++ b/examples/scripts/kagome/README.ipynb @@ -13,7 +13,7 @@ " - Eigensolving for ground states\n", " - The `SpinConserve` subspace\n", " - The `XParity` subspace\n", - " - Computing correlation functions" + " - Computing correlation functions (TODO)" ] }, { diff --git a/examples/scripts/kagome/README.md b/examples/scripts/kagome/README.md index f61c2af..8eb0e35 100644 --- a/examples/scripts/kagome/README.md +++ b/examples/scripts/kagome/README.md @@ -6,7 +6,7 @@ - Eigensolving for ground states - The `SpinConserve` subspace - The `XParity` subspace - - Computing correlation functions + - Computing correlation functions (TODO) ## Overview From 87c29745954d71fafe782468f554be01216704a6 Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Mon, 1 Apr 2024 10:43:51 -1000 Subject: [PATCH 72/73] switch SLEPc to use BVVECS on Ampere and newer GPUs --- CHANGELOG.md | 1 + src/dynamite/__init__.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e52f1f3..153165b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - Work around broken `petsc4py` and `slepc4py` builds with `pip>=23.1` (see [PETSc issue](https://gitlab.com/petsc/petsc/-/issues/1369)) - `Operator.__str__` and `Operator.table()` were formatted poorly for operators with complex coefficients - various issues in `dynamite.extras` + - Performance was bad on Ampere (e.g. A100) GPUs unless a particular SLEPc flag was set. The flag is now automatically set. ## 0.3.1 - 2023-03-07 diff --git a/src/dynamite/__init__.py b/src/dynamite/__init__.py index 309e8dc..0b57216 100644 --- a/src/dynamite/__init__.py +++ b/src/dynamite/__init__.py @@ -93,6 +93,20 @@ def _initialize(self, slepc_args=None, version_check=True, gpu=None): 'dynamite/petsc was not configured with ' 'GPU functionality') + # there is a bug (see here: https://gitlab.com/slepc/slepc/-/issues/72) + # that causes performance to be terrible on Ampere GPUs with BVMAT, + # when using complex numbers. + # therefore for GPUs with compute capability 8 or greater we use BVVECS, + # which is slightly less performant in other ways but doesn't have that bug. + if bbuild.complex_enabled(): + gpu_compute_capabilities = subprocess.check_output( + ['nvidia-smi', '--query-gpu=compute_cap', '--format=csv,noheader'], + encoding='UTF-8' + ) + max_cc = max(int(s.split('.')[0]) for s in gpu_compute_capabilities.strip().split('\n')) + if max_cc >= 8 and '-bv_type' not in slepc_args: + slepc_args += ['-bv_type', 'vecs'] + slepc_args += [ '-vec_type', 'cuda', '-mat_type', 'aijcusparse', From 0302e63e698aa5af4189df2fa419e9a7c1e9a2ea Mon Sep 17 00:00:00 2001 From: Greg Kahanamoku-Meyer Date: Fri, 29 Mar 2024 10:59:23 -1000 Subject: [PATCH 73/73] update affiliation --- .zenodo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zenodo.json b/.zenodo.json index 79459fa..d69bc93 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -3,7 +3,7 @@ "creators": [ { "orcid": "0000-0003-2174-3308", - "affiliation": "University of California at Berkeley", + "affiliation": "Massachusetts Institute of Technology", "name": "Gregory D. Kahanamoku-Meyer" }, {