From 0928a78cab9bd09135c411769700fdc424f553b2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 29 Jan 2024 16:06:34 -0500 Subject: [PATCH 001/305] Fix minumum time step to dt --- qutip/solver/sode/sode.py | 1 + qutip/tests/solver/test_stochastic.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 33ffaae96a..686f7bfb45 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -134,6 +134,7 @@ def __init__(self, rhs, options): self.rhs = rhs def integrate(self, t, copy=True): + print(t, self.t) delta_t = t - self.t if delta_t < 0: raise ValueError("Stochastic integration time") diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index ebbf194daf..3cbf4958df 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -348,23 +348,23 @@ def func(t, A, W): def test_deprecation_warnings(): with pytest.warns(FutureWarning, match=r'map_func'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], map_func=None) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], map_func=None) with pytest.warns(FutureWarning, match=r'progress_bar'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], progress_bar=None) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], progress_bar=None) with pytest.warns(FutureWarning, match=r'nsubsteps'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], nsubsteps=None) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], nsubsteps=None) with pytest.warns(FutureWarning, match=r'map_func'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], map_func=None) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], map_func=None) with pytest.warns(FutureWarning, match=r'store_all_expect'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], store_all_expect=1) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], store_all_expect=1) with pytest.warns(FutureWarning, match=r'store_measurement'): - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], store_measurement=1) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], store_measurement=1) with pytest.raises(TypeError) as err: - ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], m_ops=1) + ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], m_ops=1) assert '"m_ops" and "dW_factors"' in str(err.value) From eea43d462fc4b0b79c5da0fb8581194013c38464 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Jan 2024 08:52:57 -0500 Subject: [PATCH 002/305] Start run_from_exp --- qutip/solver/multitraj.py | 5 --- qutip/solver/sode/_noise.py | 31 +++++++++++++++- qutip/solver/sode/sode.py | 1 - qutip/solver/stochastic.py | 71 ++++++++++++++++++++++++++++++++++--- 4 files changed, 97 insertions(+), 11 deletions(-) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 60f52a7202..ae90aa030a 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -285,11 +285,6 @@ def _get_generator(self, seed): If the ``seed`` has a ``random`` method, it will be used as the generator. """ - if hasattr(seed, 'random'): - # We check for the method, not the type to accept pseudo non-random - # generator for debug/testing purpose. - return seed - if self.options['bitgenerator']: bit_gen = getattr(np.random, self.options['bitgenerator']) generator = np.random.Generator(bit_gen(seed)) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index e1b4592a49..6ae5b09697 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -1,6 +1,6 @@ import numpy as np -__all__ = ["Wiener"] +__all__ = ["Wiener", "PreSetWiener"] class Wiener: @@ -37,6 +37,35 @@ def __call__(self, t): return self.process[idx, 0, :] +class PreSetWiener(Wiener): + def __init__(self, noise, tlist, n_sc_ops): + if noise.shape != (len(tlist)-1, n_sc_ops): + raise ValueError( + "Noise shape must be (len(tlist)-1, nu,sc_ops)" + ) + + self.noise = noise + self.t0 = tlist[0] + self.t_last = tlist[-1] + self.dt = tlist[1] - tlist[0] + + def dW(self, t, N): + if t + N * self.dt > self.t_last: + raise ValueError( + "Requested time is outside the integration range." + ) + idx0 = int((t - self.t0) // self.dt) + return self.noise[idx0:idx0 + N, :] + + def __call__(self, t): + if t + N * self.dt > self.t_last: + raise ValueError( + "Requested time is outside the integration range." + ) + idx0 = int((t - self.t0) // self.dt) + return np.cumsum(self.noise[idx0, :], axis=0) + + class _Noise: """ Wiener process generator used for tests. diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 686f7bfb45..33ffaae96a 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -134,7 +134,6 @@ def __init__(self, rhs, options): self.rhs = rhs def integrate(self, t, copy=True): - print(t, self.t) delta_t = t - self.t if delta_t < 0: raise ValueError("Stochastic integration time") diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index f548b2a907..dd5035e420 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -590,6 +590,63 @@ def m_ops(self, new_m_ops): def dW_factors(self): return self._dW_factors + def run_from_experiment( + self, state, tlist, noise, *, + args=None, e_ops=(), type="dW", + ): + """ + Run a single trajectory from a given state and noise. + + Parameters + ---------- + state : Qobj + Initial state of the system. + + tlist : array_like + List of times for which to evaluate the state. + + noise : array_like + List of noise for each stochastic collapse operators. + + args : dict, optional + Arguments to pass to the Hamiltonian and collapse operators. + + e_ops : list, optional + List of operators for which to evaluate expectation values. + + type : str, default : "dW" + Type of noise, either Wiener increment "dW", or measurement "J". + (Measurement is nor implemented yet!) + + Returns + ------- + result : StochasticTrajResult + Result of the trajectory. + """ + result = self._resultclass( + e_ops, + self.options, + m_ops=self.m_ops, + dw_factor=self.dW_factors, + heterodyne=self.heterodyne, + ) + dt = tlist[1] - tlist[0] + if not np.allclose(dt, np.diff(tlist)): + raise ValueError("tlist must be evenly spaced.") + generator = self.PreSetWiener(noise, tlist, len(self.rhs.sc_ops)) + try: + old_dt = None + if "dt" in self._integrator.options: + old_dt = self._integrator.options["dt"] + self._integrator.options["dt"] = dt + result = self._run_inner_traj_loop(generator, state, tlist, e_ops) + except Exception as err: + if old_dt is not None: + self._integrator.options["dt"] = old_dt + raise + + return result + @dW_factors.setter def dW_factors(self, new_dW_factors): """ @@ -609,18 +666,17 @@ def dW_factors(self, new_dW_factors): ) self._dW_factors = new_dW_factors - def _run_one_traj(self, seed, state, tlist, e_ops): + def _run_inner_traj_loop(self, generator, state, tlist, e_ops): """ - Run one trajectory and return the result. + Run the main loop of a trajectory and return the result. """ - result = StochasticTrajResult( + result = self._resultclass( e_ops, self.options, m_ops=self.m_ops, dw_factor=self.dW_factors, heterodyne=self.heterodyne, ) - generator = self._get_generator(seed) self._integrator.set_state(tlist[0], state, generator) state_t = self._restore_state(state, copy=False) result.add(tlist[0], state_t, None) @@ -630,6 +686,13 @@ def _run_one_traj(self, seed, state, tlist, e_ops): result.add(t, state_t, noise) return seed, result + def _run_one_traj(self, seed, state, tlist, e_ops): + """ + Run one trajectory and return the result. + """ + generator = self._get_generator(seed) + return self._run_inner_traj_loop(generator, state, tlist, e_ops) + @classmethod def avail_integrators(cls): if cls is StochasticSolver: From 852a4467bbadace95ae70abd7c740d574010221d Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Jan 2024 17:07:29 -0500 Subject: [PATCH 003/305] Add support for measurement noise instead of Wiener noise --- qutip/solver/sode/_sode.pyx | 48 +++++++++++++++++++++-- qutip/solver/sode/itotaylor.py | 12 ++++++ qutip/solver/sode/sode.py | 10 ++++- qutip/solver/sode/ssystem.pxd | 3 ++ qutip/solver/sode/ssystem.pyx | 55 ++++++++++++++++++++++++++- qutip/solver/stochastic.py | 26 +++++++++---- qutip/tests/solver/test_stochastic.py | 14 +++---- 7 files changed, 145 insertions(+), 23 deletions(-) diff --git a/qutip/solver/sode/_sode.pyx b/qutip/solver/sode/_sode.pyx index d528228d87..5eff63c248 100644 --- a/qutip/solver/sode/_sode.pyx +++ b/qutip/solver/sode/_sode.pyx @@ -11,9 +11,11 @@ import numpy as np cdef class Euler: cdef _StochasticSystem system + cdef bint measurement_noise - def __init__(self, _StochasticSystem system): + def __init__(self, _StochasticSystem system, measurement_noise=False): self.system = system + self.measurement_noise = measurement_noise @cython.wraparound(False) def run( @@ -37,9 +39,16 @@ cdef class Euler: """ cdef int i cdef _StochasticSystem system = self.system + cdef double[:] expect cdef Data a = system.drift(t, state) b = system.diffusion(t, state) + + if self.measurement_noise: + expect = system.expect(t, state) + for i in range(system.num_collapse): + dW[0, i] -= except[i].real * dt + cdef Data new_state = _data.add(state, a, dt) for i in range(system.num_collapse): new_state = _data.add(new_state, b[i], dW[0, i]) @@ -72,6 +81,12 @@ cdef class Platen(Euler): cdef list d2 = system.diffusion(t, state) cdef Data Vt, out cdef list Vp, Vm + cdef double[:] expect + + if self.measurement_noise: + expect = system.expect(t, state) + for i in range(system.num_collapse): + dW[i] -= expect[i].real * dt out = _data.mul(d1, 0.5) Vt = d1.copy() @@ -106,6 +121,9 @@ cdef class Platen(Euler): cdef class Explicit15(Euler): + def __init__(self, _StochasticSystem system): + self.system = system + @cython.boundscheck(False) @cython.wraparound(False) @cython.cdivision(True) @@ -235,9 +253,11 @@ cdef class Explicit15(Euler): cdef class Milstein: cdef _StochasticSystem system + cdef bint measurement_noise - def __init__(self, _StochasticSystem system): - self.system = system + def __init__(self, _StochasticSystem system, measurement_noise=False): + self.system = system + self.measurement_noise = measurement_noise @cython.wraparound(False) def run(self, double t, Data state, double dt, double[:, :, ::1] dW, int ntraj): @@ -273,6 +293,11 @@ cdef class Milstein: iadd_dense(out, state, 1) iadd_dense(out, system.a(), dt) + if self.measurement_noise: + expect = system.expect(t, state) + for i in range(system.num_collapse): + dW[i] -= system.expect_i(i).real * dt + for i in range(num_ops): iadd_dense(out, system.bi(i), dW[0, i]) @@ -289,11 +314,17 @@ cdef class PredCorr: cdef Dense euler cdef double alpha, eta cdef _StochasticSystem system + cdef bint measurement_noise - def __init__(self, _StochasticSystem system, double alpha=0., double eta=0.5): + def __init__( + self, _StochasticSystem system, + double alpha=0., double eta=0.5, + measurement_noise=False + ): self.system = system self.alpha = alpha self.eta = eta + self.measurement_noise = measurement_noise @cython.wraparound(False) def run(self, double t, Data state, double dt, double[:, :, ::1] dW, int ntraj): @@ -324,6 +355,11 @@ cdef class PredCorr: system.set_state(t, state) + if self.measurement_noise: + expect = system.expect(t, state) + for i in range(system.num_collapse): + dW[i] -= system.expect_i(i).real * dt + imul_dense(out, 0.) iadd_dense(out, state, 1) iadd_dense(out, system.a(), dt * (1-alpha)) @@ -350,6 +386,10 @@ cdef class PredCorr: cdef class Taylor15(Milstein): + def __init__(self, _StochasticSystem system): + self.system = system + self.measurement_noise = False + @cython.boundscheck(False) @cython.wraparound(False) cdef Data step(self, double t, Dense state, double dt, double[:, :] dW, Dense out): diff --git a/qutip/solver/sode/itotaylor.py b/qutip/solver/sode/itotaylor.py index 6ad4df1545..b764bfd25a 100644 --- a/qutip/solver/sode/itotaylor.py +++ b/qutip/solver/sode/itotaylor.py @@ -17,8 +17,14 @@ class EulerSODE(_Explicit_Simple_Integrator): - Order: 0.5 """ + integrator_options = { + "dt": 0.001, + "tol": 1e-10, + "_measurement_noise": False, + } stepper = _sode.Euler N_dw = 1 + _stepper_options = ["_measurement_noise"] class Milstein_SODE(_Explicit_Simple_Integrator): @@ -30,8 +36,14 @@ class Milstein_SODE(_Explicit_Simple_Integrator): - Order strong 1.0 """ + integrator_options = { + "dt": 0.001, + "tol": 1e-10, + "__measurement_noise": False, + } stepper = _sode.Milstein N_dw = 1 + _stepper_options = ["_measurement_noise"] class Taylor1_5_SODE(_Explicit_Simple_Integrator): diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 33ffaae96a..747bc9c456 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -223,9 +223,14 @@ class PlatenSODE(_Explicit_Simple_Integrator): - Order: strong 1, weak 2 """ - + integrator_options = { + "dt": 0.001, + "tol": 1e-10, + "_measurement_noise": False, + } stepper = _sode.Platen N_dw = 1 + _stepper_options = ["_measurement_noise"] class PredCorr_SODE(_Explicit_Simple_Integrator): @@ -249,10 +254,11 @@ class PredCorr_SODE(_Explicit_Simple_Integrator): "tol": 1e-10, "alpha": 0.0, "eta": 0.5, + "_measurement_noise": False, } stepper = _sode.PredCorr N_dw = 1 - _stepper_options = ["alpha", "eta"] + _stepper_options = ["alpha", "eta", "_measurement_noise"] @property def options(self): diff --git a/qutip/solver/sode/ssystem.pxd b/qutip/solver/sode/ssystem.pxd index a09212c1db..6b78ebe9f8 100644 --- a/qutip/solver/sode/ssystem.pxd +++ b/qutip/solver/sode/ssystem.pxd @@ -13,10 +13,13 @@ cdef class _StochasticSystem: cpdef list diffusion(self, t, Data state) + cpdef list expect(self, t, Data state) + cpdef void set_state(self, double t, Dense state) except * cpdef Data a(self) cpdef Data bi(self, int i) + cpdef complex expect_i(self, int i) cpdef Data Libj(self, int i, int j) cpdef Data Lia(self, int i) cpdef Data L0bi(self, int i) diff --git a/qutip/solver/sode/ssystem.pyx b/qutip/solver/sode/ssystem.pyx index be15d4634a..0ad2ce9c28 100644 --- a/qutip/solver/sode/ssystem.pyx +++ b/qutip/solver/sode/ssystem.pyx @@ -51,6 +51,12 @@ cdef class _StochasticSystem: """ raise NotImplementedError + cpdef list expect(self, t, Data state): + """ + Compute the expectation terms for the ``state`` at time ``t``. + """ + raise NotImplementedError + cpdef void set_state(self, double t, Dense state) except *: """ Initialize the set of derrivatives. @@ -69,6 +75,12 @@ cdef class _StochasticSystem: """ raise NotImplementedError + cpdef complex expect_i(self, int i): + """ + Expectation value of the ``i``th operator. + """ + raise NotImplementedError + cpdef Data Libj(self, int i, int j): """ bi_n * d bj / dx_n @@ -144,7 +156,7 @@ cdef class StochasticClosedSystem(_StochasticSystem): cpdef list diffusion(self, t, Data state): cdef int i cdef QobjEvo c_op - out = [] + cdef list out = [] for i in range(self.num_collapse): c_op = self.c_ops[i] _out = c_op.matmul_data(t, state) @@ -153,6 +165,15 @@ cdef class StochasticClosedSystem(_StochasticSystem): out.append(_data.add(_out, state, -0.5 * expect)) return out + cpdef list expect(self, t, Data state): + cdef int i + cdef QobjEvo c_op + cdef list expect = [] + for i in range(self.num_collapse): + c_op = self.cpcd_ops[i] + expect.append(c_op.expect_data(t, state)) + return expect + def __reduce__(self): return ( StochasticClosedSystem.restore, @@ -210,7 +231,7 @@ cdef class StochasticOpenSystem(_StochasticSystem): cdef int i cdef QobjEvo c_op cdef complex expect - cdef out = [] + cdef list out = [] for i in range(self.num_collapse): c_op = self.c_ops[i] vec = c_op.matmul_data(t, state) @@ -218,6 +239,16 @@ cdef class StochasticOpenSystem(_StochasticSystem): out.append(_data.add(vec, state, -expect)) return out + cpdef list expect(self, t, Data, state): + cdef int i + cdef QobjEvo c_op + cdef list expect = [] + for i in range(self.num_collapse): + c_op = self.c_ops[i] + vec = c_op.matmul_data(t, state) + expect.append(_data.trace_oper_ket(vec)) + return expect + cpdef void set_state(self, double t, Dense state) except *: cdef n, l self.t = t @@ -277,6 +308,16 @@ cdef class StochasticOpenSystem(_StochasticSystem): self._compute_b() return _dense_wrap(self._b[i, :]) + cpdef complex expect_i(self, int i): + if not self._is_set: + raise RuntimeError( + "Derrivatives set for ito taylor expansion need " + "to receive the state with `set_state`." + ) + if not self._b_set: + self._compute_b() + return self.expect_Cv[i] + @cython.boundscheck(False) @cython.wraparound(False) cdef void _compute_b(self) except *: @@ -510,6 +551,13 @@ cdef class SimpleStochasticSystem(_StochasticSystem): out.append(self.c_ops[i].matmul_data(t, state)) return out + cpdef list expect(self, t, Data state): + cdef int i + cdef list expect = [] + for i in range(self.num_collapse): + expect.append(0j) + return expect + cpdef void set_state(self, double t, Dense state) except *: self.t = t self.state = state @@ -520,6 +568,9 @@ cdef class SimpleStochasticSystem(_StochasticSystem): cpdef Data bi(self, int i): return self.c_ops[i].matmul_data(self.t, self.state) + cpdef complex expect_i(self, int i): + return 0j + cpdef Data Libj(self, int i, int j): bj = self.c_ops[i].matmul_data(self.t, self.state) return self.c_ops[j].matmul_data(self.t, bj) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index dd5035e420..c1ae120aac 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -592,7 +592,7 @@ def dW_factors(self): def run_from_experiment( self, state, tlist, noise, *, - args=None, e_ops=(), type="dW", + args=None, e_ops=(), measurement=False, ): """ Run a single trajectory from a given state and noise. @@ -603,10 +603,15 @@ def run_from_experiment( Initial state of the system. tlist : array_like - List of times for which to evaluate the state. + List of times for which to evaluate the state. The tlist must + increase uniformly. noise : array_like - List of noise for each stochastic collapse operators. + Noise for each time step and each stochastic collapse operators. + ``noise[i, j]`` is the Wiener increments between ``tlist[t]`` and + ``tlist[i+1]`` for the j-th sc_ops (homodyne detection). + For heterodyne detection, the sc_ops are doubled and the i-th + operators correspond to the (2*i, 2*i+1) noise entries. args : dict, optional Arguments to pass to the Hamiltonian and collapse operators. @@ -614,9 +619,12 @@ def run_from_experiment( e_ops : list, optional List of operators for which to evaluate expectation values. - type : str, default : "dW" - Type of noise, either Wiener increment "dW", or measurement "J". - (Measurement is nor implemented yet!) + measurement : bool, default : False + Whether the passed noise is the Wiener increments (gaussian noise + with standard derivation of dt**0.5), or the measurement: + + noise[t...t+dt, i] = dW[t...t+dt, i] + + expect(sc_ops[i] + sc_ops[i].dag, state_t) * dt Returns ------- @@ -639,10 +647,12 @@ def run_from_experiment( if "dt" in self._integrator.options: old_dt = self._integrator.options["dt"] self._integrator.options["dt"] = dt + self._integrator.options["_measurement_noise"] = measurement result = self._run_inner_traj_loop(generator, state, tlist, e_ops) except Exception as err: if old_dt is not None: self._integrator.options["dt"] = old_dt + self._integrator.options["_measurement_noise"] = dt raise return result @@ -684,14 +694,14 @@ def _run_inner_traj_loop(self, generator, state, tlist, e_ops): t, state, noise = self._integrator.integrate(t, copy=False) state_t = self._restore_state(state, copy=False) result.add(t, state_t, noise) - return seed, result + return result def _run_one_traj(self, seed, state, tlist, e_ops): """ Run one trajectory and return the result. """ generator = self._get_generator(seed) - return self._run_inner_traj_loop(generator, state, tlist, e_ops) + return seed, self._run_inner_traj_loop(generator, state, tlist, e_ops) @classmethod def avail_integrators(cls): diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 3cbf4958df..ebbf194daf 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -348,23 +348,23 @@ def func(t, A, W): def test_deprecation_warnings(): with pytest.warns(FutureWarning, match=r'map_func'): - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], map_func=None) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], map_func=None) with pytest.warns(FutureWarning, match=r'progress_bar'): - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], progress_bar=None) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], progress_bar=None) with pytest.warns(FutureWarning, match=r'nsubsteps'): - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], nsubsteps=None) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], nsubsteps=None) with pytest.warns(FutureWarning, match=r'map_func'): - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], map_func=None) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], map_func=None) with pytest.warns(FutureWarning, match=r'store_all_expect'): - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], store_all_expect=1) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], store_all_expect=1) with pytest.warns(FutureWarning, match=r'store_measurement'): - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], store_measurement=1) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], store_measurement=1) with pytest.raises(TypeError) as err: - ssesolve(qeye(2), basis(2), [0, 1e-3], [qeye(2)], m_ops=1) + ssesolve(qeye(2), basis(2), [0, 1e-5], [qeye(2)], m_ops=1) assert '"m_ops" and "dW_factors"' in str(err.value) From c54d5bd9779e5f160d2a1e0e54a1ec8ac7957f1a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 31 Jan 2024 15:07:01 -0500 Subject: [PATCH 004/305] Control in measurement and pre set Weiner --- qutip/solver/sode/_noise.py | 90 ++++++++++++------- qutip/solver/sode/itotaylor.py | 6 +- qutip/solver/sode/sode.py | 30 +++++-- qutip/solver/stochastic.py | 153 ++++++++++++++++++--------------- 4 files changed, 166 insertions(+), 113 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index 6ae5b09697..d6bb073525 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -10,60 +10,86 @@ class Wiener: def __init__(self, t0, dt, generator, shape): self.t0 = t0 self.dt = dt - self.generator = generator self.t_end = t0 self.shape = shape - self.process = np.zeros((1,) + shape, dtype=float) + self.generator = generator + self.noise = np.zeros((0,) + shape, dtype=float) + self.last_W = np.zeros(shape[-1], dtype=float) + self.idx_last_0 = 0 def _extend(self, t): N_new_vals = int((t - self.t_end + self.dt*0.01) // self.dt) dW = self.generator.normal( 0, np.sqrt(self.dt), size=(N_new_vals,) + self.shape ) + self.noise = np.concatenate((self.noise, dW), axis=0) W = self.process[-1, :, :] + np.cumsum(dW, axis=0) self.process = np.concatenate((self.process, W), axis=0) self.t_end = self.t0 + (self.process.shape[0] - 1) * self.dt def dW(self, t, N): - if t + N * self.dt > self.t_end: - self._extend(t + N * self.dt) - idx0 = int((t - self.t0 + self.dt * 0.01) // self.dt) - return np.diff(self.process[idx0:idx0 + N + 1, :, :], axis=0) + # Find the index of t. + # Rounded to the closest step, but only multiple of dt are expected. + idx0 = int((t - self.t0 + self.dt * 0.4999) // self.dt) + if idx0 + N >= self.noise.shape[0]: + self._extend(idx0 + N - 1) + return self.noise[idx0:idx0 + N, :, :] def __call__(self, t): - if t > self.t_end: - self._extend(t) - idx = int((t - self.t0 + self.dt * 0.01) // self.dt) - return self.process[idx, 0, :] + """ + Return the Wiener process at the closest ``dt`` step to ``t``. + """ + # The Wiener process is not used directly in the evolution, so it's + # less optimized than the ``dW`` method. + + # Find the index of t. + # Rounded to the closest step, but only multiple of dt are expected. + idx0 = int((t - self.t0 + self.dt * 0.4999) // self.dt) + if idx0 >= self.noise.shape[0]: + self._extend(idx0) + + if self.idx_last_0 > idx: + # Before last call, reseting + self.idx_last_0 = 0 + self.last_W = np.zeros(self.shape[-1], dtype=float) + + self.last_W = self.last_W + np.sum( + self.noise[self.idx_last_0:idx+1, 0, :], axis=0 + ) + + self.idx_last_0 = idx + return self.last_W class PreSetWiener(Wiener): - def __init__(self, noise, tlist, n_sc_ops): - if noise.shape != (len(tlist)-1, n_sc_ops): - raise ValueError( - "Noise shape must be (len(tlist)-1, nu,sc_ops)" - ) + def __init__(self, noise, tlist, n_sc_ops, heterodyne, is_measurement): + if heterodyne: + if noise.shape != (n_sc_ops/2, 2, len(tlist)-1): + raise ValueError( + "Noise is not of the expected shape: " + f"{(n_sc_ops/2, 2, len(tlist)-1))}" + ) + noise = np.reshape(noise, (n_sc_ops, len(tlist)-1), "C") + else: + if noise.shape != (n_sc_ops, len(tlist)-1): + raise ValueError( + "Noise is not of the expected shape: " + f"{(n_sc_ops, len(tlist)-1))}" + ) - self.noise = noise self.t0 = tlist[0] - self.t_last = tlist[-1] + self.t_end = tlist[-1] self.dt = tlist[1] - tlist[0] + self.shape = noise.shape[1:] + self.noise = noise.T[:, np.newaxis, :] + self.last_W = np.zeros(self.shape[-1], dtype=float) + self.idx_last_0 = 0 + self.is_measurement = is_measurement - def dW(self, t, N): - if t + N * self.dt > self.t_last: - raise ValueError( - "Requested time is outside the integration range." - ) - idx0 = int((t - self.t0) // self.dt) - return self.noise[idx0:idx0 + N, :] - - def __call__(self, t): - if t + N * self.dt > self.t_last: - raise ValueError( - "Requested time is outside the integration range." - ) - idx0 = int((t - self.t0) // self.dt) - return np.cumsum(self.noise[idx0, :], axis=0) + def _extend(self, t): + raise ValueError( + "Requested time is outside the integration range." + ) class _Noise: diff --git a/qutip/solver/sode/itotaylor.py b/qutip/solver/sode/itotaylor.py index b764bfd25a..a4541206cd 100644 --- a/qutip/solver/sode/itotaylor.py +++ b/qutip/solver/sode/itotaylor.py @@ -20,11 +20,10 @@ class EulerSODE(_Explicit_Simple_Integrator): integrator_options = { "dt": 0.001, "tol": 1e-10, - "_measurement_noise": False, } stepper = _sode.Euler N_dw = 1 - _stepper_options = ["_measurement_noise"] + _stepper_options = ["measurement_noise"] class Milstein_SODE(_Explicit_Simple_Integrator): @@ -39,11 +38,10 @@ class Milstein_SODE(_Explicit_Simple_Integrator): integrator_options = { "dt": 0.001, "tol": 1e-10, - "__measurement_noise": False, } stepper = _sode.Milstein N_dw = 1 - _stepper_options = ["_measurement_noise"] + _stepper_options = ["measurement_noise"] class Taylor1_5_SODE(_Explicit_Simple_Integrator): diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 747bc9c456..d1b4b12e41 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -2,7 +2,7 @@ from . import _sode from ..integrator.integrator import Integrator from ..stochastic import StochasticSolver, SMESolver -from ._noise import Wiener +from ._noise import Wiener, PreSetWiener __all__ = ["SIntegrator", "PlatenSODE", "PredCorr_SODE"] @@ -64,7 +64,24 @@ def set_state(self, t, state0, generator): """ self.t = t self.state = state0 - if isinstance(generator, Wiener): + stepper_opt = { + key: self.options[key] + for key in self._stepper_options + if key in self.options + } + + if isinstance(generator, PreSetWiener): + self.wiener = generator + if ( + generator.is_measurements + and "measurement_noise" not in self._stepper_options + ): + raise NotImplementedError( + f"{type(self).__name__} does not support running" + " the evolution from measurements." + ) + stepper_opt["measurement_noise"] = generator.has_measurements + elif isinstance(generator, Wiener): self.wiener = generator else: num_collapse = len(self.rhs.sc_ops) @@ -73,8 +90,7 @@ def set_state(self, t, state0, generator): (self.N_dw, num_collapse) ) self.rhs._register_feedback(self.wiener) - opt = [self.options[key] for key in self._stepper_options] - self.step_func = self.stepper(self.rhs(self.options), *opt).run + self.step_func = self.stepper(self.rhs(self.options), **opt).run self._is_set = True def get_state(self, copy=True): @@ -226,11 +242,10 @@ class PlatenSODE(_Explicit_Simple_Integrator): integrator_options = { "dt": 0.001, "tol": 1e-10, - "_measurement_noise": False, } stepper = _sode.Platen N_dw = 1 - _stepper_options = ["_measurement_noise"] + _stepper_options = ["measurement_noise"] class PredCorr_SODE(_Explicit_Simple_Integrator): @@ -254,11 +269,10 @@ class PredCorr_SODE(_Explicit_Simple_Integrator): "tol": 1e-10, "alpha": 0.0, "eta": 0.5, - "_measurement_noise": False, } stepper = _sode.PredCorr N_dw = 1 - _stepper_options = ["alpha", "eta", "_measurement_noise"] + _stepper_options = ["alpha", "eta", "measurement_noise"] @property def options(self): diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index c1ae120aac..6d470e0610 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -75,7 +75,15 @@ def measurement(self): for heterodyne detection. """ dts = np.diff(self.times) - m_expect = np.array(self.m_expect)[:, 1:] + if self.options["store_measurement"] == "start": + m_expect = np.array(self.m_expect)[:, :-1] + elif self.options["store_measurement"] == "middle": + m_expect = np.apply_along_axis( + lambda m: np.convolve(m, [0.5, 0.5], "valid"), + axis=0, arr=self.m_expect, + ) + elif self.options["store_measurement"] in ["start", True]: + m_expect = np.array(self.m_expect)[:, 1:] noise = np.einsum( "i,ij,j->ij", self.dW_factor, np.diff(self.W, axis=1), (1 / dts) ) @@ -316,9 +324,10 @@ def smesolve( | Whether or not to store the state vectors or density matrices. On `None` the states will be saved if no expectation operators are given. - - | store_measurement: bool - | Whether to store the measurement and wiener process for each - trajectories. + - | store_measurement: str, {'start', 'middle', 'end', ''} + | Whether and how to store the measurement for each trajectories. + 'start', 'middle', 'end' indicate when in the interval the + expectation value of the ``m_ops`` is taken. - | keep_runs_results : bool | Whether to store results from all trajectories or just store the averages. @@ -436,9 +445,10 @@ def ssesolve( | Whether or not to store the state vectors or density matrices. On `None` the states will be saved if no expectation operators are given. - - | store_measurement: bool - Whether to store the measurement and wiener process, or brownian - noise for each trajectories. + - | store_measurement: str, {'start', 'middle', 'end', ''} + | Whether and how to store the measurement for each trajectories. + 'start', 'middle', 'end' indicate when in the interval the + expectation value of the ``m_ops`` is taken. - | keep_runs_results : bool | Whether to store results from all trajectories or just store the averages. @@ -498,7 +508,7 @@ class StochasticSolver(MultiTrajSolver): "progress_kwargs": {"chunk_size": 10}, "store_final_state": False, "store_states": None, - "store_measurement": False, + "store_measurement": "", "keep_runs_results": False, "normalize_output": False, "method": "taylor1.5", @@ -590,6 +600,52 @@ def m_ops(self, new_m_ops): def dW_factors(self): return self._dW_factors + @dW_factors.setter + def dW_factors(self, new_dW_factors): + """ + Scaling of the noise on the measurements. + Default are ``1`` for homodyne and ``sqrt(1/2)`` for heterodyne. + ``dW_factors`` must be a list of the same length as ``m_ops``. + """ + if len(new_dW_factors) != len(self._dW_factors): + if self.heterodyne: + raise ValueError( + f"2 `dW_factors` per `sc_ops`, {len(self.rhs.sc_ops)} " + "values are expected for heterodyne measurement." + ) + else: + raise ValueError( + f"{len(self.rhs.sc_ops)} dW_factors are expected." + ) + self._dW_factors = new_dW_factors + + def _run_inner_traj_loop(self, generator, state, tlist, e_ops): + """ + Run the main loop of a trajectory and return the result. + """ + result = self._resultclass( + e_ops, + self.options, + m_ops=self.m_ops, + dw_factor=self.dW_factors, + heterodyne=self.heterodyne, + ) + self._integrator.set_state(tlist[0], state, generator) + state_t = self._restore_state(state, copy=False) + result.add(tlist[0], state_t, None) + for t in tlist[1:]: + t, state, noise = self._integrator.integrate(t, copy=False) + state_t = self._restore_state(state, copy=False) + result.add(t, state_t, noise) + return result + + def _run_one_traj(self, seed, state, tlist, e_ops): + """ + Run one trajectory and return the result. + """ + generator = self._get_generator(seed) + return seed, self._run_inner_traj_loop(generator, state, tlist, e_ops) + def run_from_experiment( self, state, tlist, noise, *, args=None, e_ops=(), measurement=False, @@ -608,10 +664,11 @@ def run_from_experiment( noise : array_like Noise for each time step and each stochastic collapse operators. - ``noise[i, j]`` is the Wiener increments between ``tlist[t]`` and - ``tlist[i+1]`` for the j-th sc_ops (homodyne detection). - For heterodyne detection, the sc_ops are doubled and the i-th - operators correspond to the (2*i, 2*i+1) noise entries. + For homodyne detection, ``noise[i, t_idx]`` is the Wiener + increments between ``tlist[t_idx]`` and ``tlist[t_idx+1]`` for the + i-th sc_ops. + For heterodyne detection, an extra dimension is added for the pair + of measurement: ``noise[i, j, t_idx]``with ``j`` in ``{0,1}``. args : dict, optional Arguments to pass to the Hamiltonian and collapse operators. @@ -620,11 +677,13 @@ def run_from_experiment( List of operators for which to evaluate expectation values. measurement : bool, default : False - Whether the passed noise is the Wiener increments (gaussian noise - with standard derivation of dt**0.5), or the measurement: + Whether the passed noise is the Wiener increments ``dW`` (gaussian + noise with standard derivation of dt**0.5), or the measurement: + + noise = dW/dt + expect(sc_ops[i] + sc_ops[i].dag, state_t) - noise[t...t+dt, i] = dW[t...t+dt, i] + - expect(sc_ops[i] + sc_ops[i].dag, state_t) * dt + Note that the expectation value is usally computed at the start of + the step. Only available for limited integration methods. Returns ------- @@ -641,68 +700,22 @@ def run_from_experiment( dt = tlist[1] - tlist[0] if not np.allclose(dt, np.diff(tlist)): raise ValueError("tlist must be evenly spaced.") - generator = self.PreSetWiener(noise, tlist, len(self.rhs.sc_ops)) + generator = self.PreSetWiener( + noise, tlist, len(self.rhs.sc_ops), self.heterodyne, measurement + ) try: old_dt = None if "dt" in self._integrator.options: old_dt = self._integrator.options["dt"] self._integrator.options["dt"] = dt - self._integrator.options["_measurement_noise"] = measurement result = self._run_inner_traj_loop(generator, state, tlist, e_ops) except Exception as err: if old_dt is not None: self._integrator.options["dt"] = old_dt - self._integrator.options["_measurement_noise"] = dt raise return result - @dW_factors.setter - def dW_factors(self, new_dW_factors): - """ - Scaling of the noise on the measurements. - Default are ``1`` for homodyne and ``sqrt(1/2)`` for heterodyne. - ``dW_factors`` must be a list of the same length as ``m_ops``. - """ - if len(new_dW_factors) != len(self._dW_factors): - if self.heterodyne: - raise ValueError( - f"2 `dW_factors` per `sc_ops`, {len(self.rhs.sc_ops)} " - "values are expected for heterodyne measurement." - ) - else: - raise ValueError( - f"{len(self.rhs.sc_ops)} dW_factors are expected." - ) - self._dW_factors = new_dW_factors - - def _run_inner_traj_loop(self, generator, state, tlist, e_ops): - """ - Run the main loop of a trajectory and return the result. - """ - result = self._resultclass( - e_ops, - self.options, - m_ops=self.m_ops, - dw_factor=self.dW_factors, - heterodyne=self.heterodyne, - ) - self._integrator.set_state(tlist[0], state, generator) - state_t = self._restore_state(state, copy=False) - result.add(tlist[0], state_t, None) - for t in tlist[1:]: - t, state, noise = self._integrator.integrate(t, copy=False) - state_t = self._restore_state(state, copy=False) - result.add(t, state_t, noise) - return result - - def _run_one_traj(self, seed, state, tlist, e_ops): - """ - Run one trajectory and return the result. - """ - generator = self._get_generator(seed) - return seed, self._run_inner_traj_loop(generator, state, tlist, e_ops) - @classmethod def avail_integrators(cls): if cls is StochasticSolver: @@ -726,8 +739,10 @@ def options(self): On `None` the states will be saved if no expectation operators are given. - store_measurement: bool, default: False - Whether to store the measurement for each trajectories. + store_measurement: str, {'start', 'middle', 'end', ''}, default: "" + Whether and how to store the measurement for each trajectories. + 'start', 'middle', 'end' indicate when in the interval the + expectation value of the ``m_ops`` is taken. Storing measurements will also store the wiener process, or brownian noise for each trajectories. @@ -856,7 +871,7 @@ class SMESolver(StochasticSolver): "progress_kwargs": {"chunk_size": 10}, "store_final_state": False, "store_states": None, - "store_measurement": False, + "store_measurement": "", "keep_runs_results": False, "normalize_output": False, "method": "platen", @@ -899,7 +914,7 @@ class SSESolver(StochasticSolver): "progress_kwargs": {"chunk_size": 10}, "store_final_state": False, "store_states": None, - "store_measurement": False, + "store_measurement": "", "keep_runs_results": False, "normalize_output": False, "method": "platen", From fbafbbd331465f2b3d653a538b2af0a7ab21b40c Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 31 Jan 2024 15:56:48 -0500 Subject: [PATCH 005/305] Fix bugs --- qutip/solver/sode/_noise.py | 21 ++++++++------------- qutip/solver/sode/_sode.pyx | 14 +++++++------- qutip/solver/sode/sode.py | 3 ++- qutip/solver/sode/ssystem.pyx | 2 +- qutip/solver/stochastic.py | 3 ++- qutip/tests/solver/test_stochastic.py | 5 +++-- 6 files changed, 23 insertions(+), 25 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index d6bb073525..91ed9531b9 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -10,29 +10,25 @@ class Wiener: def __init__(self, t0, dt, generator, shape): self.t0 = t0 self.dt = dt - self.t_end = t0 self.shape = shape self.generator = generator self.noise = np.zeros((0,) + shape, dtype=float) self.last_W = np.zeros(shape[-1], dtype=float) self.idx_last_0 = 0 - def _extend(self, t): - N_new_vals = int((t - self.t_end + self.dt*0.01) // self.dt) + def _extend(self, idx): + N_new_vals = idx - self.noise.shape[0] dW = self.generator.normal( 0, np.sqrt(self.dt), size=(N_new_vals,) + self.shape ) self.noise = np.concatenate((self.noise, dW), axis=0) - W = self.process[-1, :, :] + np.cumsum(dW, axis=0) - self.process = np.concatenate((self.process, W), axis=0) - self.t_end = self.t0 + (self.process.shape[0] - 1) * self.dt def dW(self, t, N): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. idx0 = int((t - self.t0 + self.dt * 0.4999) // self.dt) if idx0 + N >= self.noise.shape[0]: - self._extend(idx0 + N - 1) + self._extend(idx0 + N) return self.noise[idx0:idx0 + N, :, :] def __call__(self, t): @@ -44,9 +40,9 @@ def __call__(self, t): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. - idx0 = int((t - self.t0 + self.dt * 0.4999) // self.dt) - if idx0 >= self.noise.shape[0]: - self._extend(idx0) + idx = int((t - self.t0 + self.dt * 0.4999) // self.dt) + if idx >= self.noise.shape[0]: + self._extend(idx + 1) if self.idx_last_0 > idx: # Before last call, reseting @@ -67,18 +63,17 @@ def __init__(self, noise, tlist, n_sc_ops, heterodyne, is_measurement): if noise.shape != (n_sc_ops/2, 2, len(tlist)-1): raise ValueError( "Noise is not of the expected shape: " - f"{(n_sc_ops/2, 2, len(tlist)-1))}" + f"{(n_sc_ops/2, 2, len(tlist)-1)}" ) noise = np.reshape(noise, (n_sc_ops, len(tlist)-1), "C") else: if noise.shape != (n_sc_ops, len(tlist)-1): raise ValueError( "Noise is not of the expected shape: " - f"{(n_sc_ops, len(tlist)-1))}" + f"{(n_sc_ops, len(tlist)-1)}" ) self.t0 = tlist[0] - self.t_end = tlist[-1] self.dt = tlist[1] - tlist[0] self.shape = noise.shape[1:] self.noise = noise.T[:, np.newaxis, :] diff --git a/qutip/solver/sode/_sode.pyx b/qutip/solver/sode/_sode.pyx index 5eff63c248..95e90c9cac 100644 --- a/qutip/solver/sode/_sode.pyx +++ b/qutip/solver/sode/_sode.pyx @@ -47,7 +47,7 @@ cdef class Euler: if self.measurement_noise: expect = system.expect(t, state) for i in range(system.num_collapse): - dW[0, i] -= except[i].real * dt + dW[0, i] -= expect[i].real * dt cdef Data new_state = _data.add(state, a, dt) for i in range(system.num_collapse): @@ -86,7 +86,7 @@ cdef class Platen(Euler): if self.measurement_noise: expect = system.expect(t, state) for i in range(system.num_collapse): - dW[i] -= expect[i].real * dt + dW[0, i] -= expect[i].real * dt out = _data.mul(d1, 0.5) Vt = d1.copy() @@ -296,7 +296,7 @@ cdef class Milstein: if self.measurement_noise: expect = system.expect(t, state) for i in range(system.num_collapse): - dW[i] -= system.expect_i(i).real * dt + dW[0, i] -= system.expect_i(i).real * dt for i in range(num_ops): iadd_dense(out, system.bi(i), dW[0, i]) @@ -358,7 +358,7 @@ cdef class PredCorr: if self.measurement_noise: expect = system.expect(t, state) for i in range(system.num_collapse): - dW[i] -= system.expect_i(i).real * dt + dW[0, i] -= system.expect_i(i).real * dt imul_dense(out, 0.) iadd_dense(out, state, 1) @@ -438,17 +438,17 @@ cdef class Milstein_imp: cdef double prev_dt cdef dict imp_opt - def __init__(self, _StochasticSystem system, imp_method=None, imp_options={}): + def __init__(self, _StochasticSystem system, solve_method=None, solve_options={}): self.system = system self.prev_dt = 0 - if imp_method == "inv": + if solve_method == "inv": if not self.system.L.isconstant: raise TypeError("The 'inv' integration method requires that the system Hamiltonian or Liouvillian be constant.") self.use_inv = True self.imp_opt = {} else: self.use_inv = False - self.imp_opt = {"method": imp_method, "options": imp_options} + self.imp_opt = {"method": solve_method, "options": solve_options} @cython.wraparound(False) diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index d1b4b12e41..84f2afc3a3 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -90,7 +90,8 @@ def set_state(self, t, state0, generator): (self.N_dw, num_collapse) ) self.rhs._register_feedback(self.wiener) - self.step_func = self.stepper(self.rhs(self.options), **opt).run + rhs = self.rhs(self.options) + self.step_func = self.stepper(rhs, **stepper_opt).run self._is_set = True def get_state(self, copy=True): diff --git a/qutip/solver/sode/ssystem.pyx b/qutip/solver/sode/ssystem.pyx index 0ad2ce9c28..88acefaea4 100644 --- a/qutip/solver/sode/ssystem.pyx +++ b/qutip/solver/sode/ssystem.pyx @@ -239,7 +239,7 @@ cdef class StochasticOpenSystem(_StochasticSystem): out.append(_data.add(vec, state, -expect)) return out - cpdef list expect(self, t, Data, state): + cpdef list expect(self, t, Data state): cdef int i cdef QobjEvo c_op cdef list expect = [] diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 6d470e0610..3f5cf0f9e8 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -500,6 +500,7 @@ class StochasticSolver(MultiTrajSolver): name = "StochasticSolver" _resultclass = StochasticResult + _trajectory_resultclass = StochasticTrajResult _avail_integrators = {} system = None _open = None @@ -623,7 +624,7 @@ def _run_inner_traj_loop(self, generator, state, tlist, e_ops): """ Run the main loop of a trajectory and return the result. """ - result = self._resultclass( + result = self._trajectory_resultclass( e_ops, self.options, m_ops=self.m_ops, diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index ebbf194daf..eca1a7951f 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -319,7 +319,6 @@ def test_m_ops(heterodyne): def test_feedback(): - tol = 0.05 N = 10 ntraj = 2 @@ -342,7 +341,9 @@ def func(t, A, W): solver = SMESolver(H, sc_ops=sc_ops, heterodyne=False, options=options) results = solver.run(psi0, times, e_ops=[num(N)], ntraj=ntraj) - assert np.all(results.expect[0] > 6.-1e-6) + # If this was deterministic, it should never go under `6`. + # We add a tolerance ~dt due to the stochatic part. + assert np.all(results.expect[0] > 6. - 0.001) assert np.all(results.expect[0][-20:] < 6.7) From 6634030decfcbde90f01d322a21d9d5cd5a21ae4 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 1 Feb 2024 16:16:18 -0500 Subject: [PATCH 006/305] Refocus around dW in results + tests --- qutip/solver/sode/_noise.py | 2 +- qutip/solver/stochastic.py | 65 ++++++++++++++++---------- qutip/tests/solver/test_sode_method.py | 6 +-- qutip/tests/solver/test_stochastic.py | 35 +++++++++++++- 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index 91ed9531b9..6878ef0d37 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -65,7 +65,7 @@ def __init__(self, noise, tlist, n_sc_ops, heterodyne, is_measurement): "Noise is not of the expected shape: " f"{(n_sc_ops/2, 2, len(tlist)-1)}" ) - noise = np.reshape(noise, (n_sc_ops, len(tlist)-1), "C") + noise = np.reshape(noise, (n_sc_ops, len(tlist)-1), "C") / 2**0.5 else: if noise.shape != (n_sc_ops, len(tlist)-1): raise ValueError( diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 3f5cf0f9e8..c3078a151d 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -13,23 +13,22 @@ class StochasticTrajResult(Result): def _post_init(self, m_ops=(), dw_factor=(), heterodyne=False): super()._post_init() - self.W = [] - self.m_ops = [] - self.m_expect = [] - self.dW_factor = dw_factor + self.noise = [] self.heterodyne = heterodyne - for op in m_ops: - f = self._e_op_func(op) - self.W.append([0.0]) - self.m_expect.append([]) - self.m_ops.append(ExpectOp(op, f, self.m_expect[-1].append)) - self.add_processor(self.m_ops[-1]._store) + if self.options["store_measurement"]: + self.m_ops = [] + self.m_expect = [] + self.dW_factor = dw_factor + for op in m_ops: + f = self._e_op_func(op) + self.m_expect.append([]) + self.m_ops.append(ExpectOp(op, f, self.m_expect[-1].append)) + self.add_processor(self.m_ops[-1]._store) def add(self, t, state, noise): super().add(t, state) - if noise is not None and self.options["store_measurement"]: - for i, dW in enumerate(noise): - self.W[i].append(self.W[i][-1] + dW) + if noise is not None: + self.noise.append(noise) @property def wiener_process(self): @@ -42,7 +41,11 @@ def wiener_process(self): (len(sc_ops), 2, len(tlist)) for heterodyne detection. """ - W = np.array(self.W) + W = np.zeros( + (self.noise[0].shape[0], len(self.times)), + dtype=np.float64 + ) + np.cumsum(np.array(self.noise).T, axis=1, out=W[:, 1:]) if self.heterodyne: W = W.reshape(-1, 2, W.shape[1]) return W @@ -58,10 +61,10 @@ def dW(self): (len(sc_ops), 2, len(tlist)-1) for heterodyne detection. """ - dw = np.diff(self.W, axis=1) + noise = np.array(self.noise).T if self.heterodyne: - dw = dw.reshape(-1, 2, dw.shape[1]) - return dw + return noise.reshape(-1, 2, noise.shape[1]) + return noise @property def measurement(self): @@ -74,23 +77,30 @@ def measurement(self): (len(sc_ops), 2, len(tlist)-1) for heterodyne detection. """ - dts = np.diff(self.times) - if self.options["store_measurement"] == "start": + if not self.options["store_measurement"]: + return None + elif self.options["store_measurement"] == "end": m_expect = np.array(self.m_expect)[:, :-1] elif self.options["store_measurement"] == "middle": m_expect = np.apply_along_axis( lambda m: np.convolve(m, [0.5, 0.5], "valid"), - axis=0, arr=self.m_expect, + axis=1, arr=self.m_expect, ) elif self.options["store_measurement"] in ["start", True]: m_expect = np.array(self.m_expect)[:, 1:] - noise = np.einsum( - "i,ij,j->ij", self.dW_factor, np.diff(self.W, axis=1), (1 / dts) + else: + raise ValueError( + "store_measurement must be in {'start', 'middle', 'end', ''}, " + f"not {self.options['store_measurement']}" + ) + noise = np.array(self.noise).T + noise_scaled = np.einsum( + "i,ij,j->ij", self.dW_factor, noise, (1 / np.diff(self.times)) ) if self.heterodyne: m_expect = m_expect.reshape(-1, 2, m_expect.shape[1]) - noise = noise.reshape(-1, 2, noise.shape[1]) - return m_expect + noise + noise_scaled = noise_scaled.reshape(-1, 2, noise_scaled.shape[1]) + return m_expect + noise_scaled class StochasticResult(MultiTrajResult): @@ -681,7 +691,8 @@ def run_from_experiment( Whether the passed noise is the Wiener increments ``dW`` (gaussian noise with standard derivation of dt**0.5), or the measurement: - noise = dW/dt + expect(sc_ops[i] + sc_ops[i].dag, state_t) + noise = dW/dt * dW_factors + + expect(sc_ops[i] + sc_ops[i].dag, state_t) Note that the expectation value is usally computed at the start of the step. Only available for limited integration methods. @@ -690,6 +701,10 @@ def run_from_experiment( ------- result : StochasticTrajResult Result of the trajectory. + + Notes + ----- + Only default values of `m_ops` and `dW_factors` are supported. """ result = self._resultclass( e_ops, diff --git a/qutip/tests/solver/test_sode_method.py b/qutip/tests/solver/test_sode_method.py index 356a2ce70a..4b47fcf79f 100644 --- a/qutip/tests/solver/test_sode_method.py +++ b/qutip/tests/solver/test_sode_method.py @@ -60,7 +60,7 @@ def _make_oper(kind, N): pytest.param("Euler", 0.5, {}, id="Euler"), pytest.param("Milstein", 1.0, {}, id="Milstein"), pytest.param("Milstein_imp", 1.0, {}, id="Milstein implicit"), - pytest.param("Milstein_imp", 1.0, {"imp_method": "inv"}, + pytest.param("Milstein_imp", 1.0, {"solver_method": "inv"}, id="Milstein implicit inv"), pytest.param("Platen", 1.0, {}, id="Platen"), pytest.param("PredCorr", 1.0, {}, id="PredCorr"), @@ -68,7 +68,7 @@ def _make_oper(kind, N): pytest.param("Taylor15", 1.5, {}, id="Taylor15"), pytest.param("Explicit15", 1.5, {}, id="Explicit15"), pytest.param("Taylor15_imp", 1.5, {}, id="Taylor15 implicit"), - pytest.param("Taylor15_imp", 1.5, {"imp_method": "inv"}, + pytest.param("Taylor15_imp", 1.5, {"solver_method": "inv"}, id="Taylor15 implicit inv"), ]) @pytest.mark.parametrize(['H', 'sc_ops'], [ @@ -79,7 +79,7 @@ def _make_oper(kind, N): pytest.param("qeye", ["qeye", "destroy", "destroy2"], id='3 sc_ops'), ]) def test_methods(H, sc_ops, method, order, kw): - if kw == {"imp_method": "inv"} and ("td" in H or "td" in sc_ops[0]): + if kw == {"solver_method": "inv"} and ("td" in H or "td" in sc_ops[0]): pytest.skip("inverse method only available for constant cases.") N = 5 H = _make_oper(H, N) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 444834fac3..c6bf3cd95e 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -281,10 +281,43 @@ def test_reuse_seeds(): @pytest.mark.parametrize("heterodyne", [True, False]) -def test_m_ops(heterodyne): +def test_measurements(heterodyne): N = 10 ntraj = 1 + H = num(N) + sc_ops = [destroy(N)] + psi0 = basis(N, N-1) + + times = np.linspace(0, 1.0, 11) + + solver = SMESolver(H, sc_ops, heterodyne=heterodyne) + + solver.options["store_measurement"] = "start" + res_start = solver.run(psi0, times, ntraj=ntraj, seeds=1) + + solver.options["store_measurement"] = "middle" + res_middle = solver.run(psi0, times, ntraj=ntraj, seeds=1) + + solver.options["store_measurement"] = "end" + res_end = solver.run(psi0, times, ntraj=ntraj, seeds=1) + + diff = np.sum(np.abs(res_end.measurement[0] - res_start.measurement[0])) + assert diff > 0.1 # Each measurement should be different by ~dt + np.testing.assert_allclose( + res_middle.measurement[0] * 2, + res_start.measurement[0] + res_end.measurement[0], + ) + + np.testing.assert_allclose( + np.diff(res_start.wiener_process[0][0]), res_start.dW[0][0] + ) + + +@pytest.mark.parametrize("heterodyne", [True, False]) +def test_m_ops(heterodyne): + N = 10 + H = num(N) sc_ops = [destroy(N), qeye(N)] psi0 = basis(N, N-1) From 22a7516e804af3e38be8deb5571afba552f6c9cf Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 6 Feb 2024 12:16:18 -0500 Subject: [PATCH 007/305] Add test for run experiment --- qutip/solver/sode/_noise.py | 15 +++-- qutip/solver/sode/_sode.pyx | 4 +- qutip/solver/sode/sode.py | 4 +- qutip/solver/stochastic.py | 25 ++++---- qutip/tests/solver/test_stochastic.py | 90 ++++++++++++++++++++++++++- 5 files changed, 117 insertions(+), 21 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index 6878ef0d37..1eb8a2e34a 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -27,7 +27,7 @@ def dW(self, t, N): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. idx0 = int((t - self.t0 + self.dt * 0.4999) // self.dt) - if idx0 + N >= self.noise.shape[0]: + if idx0 + N - 1 >= self.noise.shape[0]: self._extend(idx0 + N) return self.noise[idx0:idx0 + N, :, :] @@ -65,7 +65,7 @@ def __init__(self, noise, tlist, n_sc_ops, heterodyne, is_measurement): "Noise is not of the expected shape: " f"{(n_sc_ops/2, 2, len(tlist)-1)}" ) - noise = np.reshape(noise, (n_sc_ops, len(tlist)-1), "C") / 2**0.5 + noise = np.reshape(noise, (n_sc_ops, len(tlist)-1), "C") else: if noise.shape != (n_sc_ops, len(tlist)-1): raise ValueError( @@ -76,14 +76,19 @@ def __init__(self, noise, tlist, n_sc_ops, heterodyne, is_measurement): self.t0 = tlist[0] self.dt = tlist[1] - tlist[0] self.shape = noise.shape[1:] - self.noise = noise.T[:, np.newaxis, :] + self.noise = noise.T[:, np.newaxis, :].copy() self.last_W = np.zeros(self.shape[-1], dtype=float) self.idx_last_0 = 0 self.is_measurement = is_measurement + if self.is_measurement: + # Measurements is scaled as + dW / dt + self.noise *= self.dt + if heterodyne: + self.noise /= 2**0.5 - def _extend(self, t): + def _extend(self, N): raise ValueError( - "Requested time is outside the integration range." + f"Requested time is outside the integration range. {N} > {self.noise.shape[0]}" ) diff --git a/qutip/solver/sode/_sode.pyx b/qutip/solver/sode/_sode.pyx index 95e90c9cac..a38079686e 100644 --- a/qutip/solver/sode/_sode.pyx +++ b/qutip/solver/sode/_sode.pyx @@ -39,7 +39,7 @@ cdef class Euler: """ cdef int i cdef _StochasticSystem system = self.system - cdef double[:] expect + cdef list expect cdef Data a = system.drift(t, state) b = system.diffusion(t, state) @@ -81,7 +81,7 @@ cdef class Platen(Euler): cdef list d2 = system.diffusion(t, state) cdef Data Vt, out cdef list Vp, Vm - cdef double[:] expect + cdef list expect if self.measurement_noise: expect = system.expect(t, state) diff --git a/qutip/solver/sode/sode.py b/qutip/solver/sode/sode.py index 9f9625a49a..d76b371374 100644 --- a/qutip/solver/sode/sode.py +++ b/qutip/solver/sode/sode.py @@ -74,14 +74,14 @@ def set_state(self, t, state0, generator): if isinstance(generator, PreSetWiener): self.wiener = generator if ( - generator.is_measurements + generator.is_measurement and "measurement_noise" not in self._stepper_options ): raise NotImplementedError( f"{type(self).__name__} does not support running" " the evolution from measurements." ) - stepper_opt["measurement_noise"] = generator.has_measurements + stepper_opt["measurement_noise"] = generator.is_measurement elif isinstance(generator, Wiener): self.wiener = generator else: diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 5b2508939f..1ff8ff2cda 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,6 +1,7 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem +from .sode._noise import PreSetWiener from .result import MultiTrajResult, Result, ExpectOp from .multitraj import MultiTrajSolver from .. import Qobj, QobjEvo @@ -8,6 +9,7 @@ from functools import partial from .solver_base import _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback, _WeinerFeedback +from time import time class StochasticTrajResult(Result): @@ -79,14 +81,14 @@ def measurement(self): """ if not self.options["store_measurement"]: return None - elif self.options["store_measurement"] == "end": + elif self.options["store_measurement"] == "start": m_expect = np.array(self.m_expect)[:, :-1] elif self.options["store_measurement"] == "middle": m_expect = np.apply_along_axis( lambda m: np.convolve(m, [0.5, 0.5], "valid"), axis=1, arr=self.m_expect, ) - elif self.options["store_measurement"] in ["start", True]: + elif self.options["store_measurement"] in ["end", True]: m_expect = np.array(self.m_expect)[:, 1:] else: raise ValueError( @@ -713,30 +715,31 @@ def run_from_experiment( ----- Only default values of `m_ops` and `dW_factors` are supported. """ - result = self._resultclass( - e_ops, - self.options, - m_ops=self.m_ops, - dw_factor=self.dW_factors, - heterodyne=self.heterodyne, - ) + start_time = time() + self._argument(args) + stats = self._initialize_stats() dt = tlist[1] - tlist[0] if not np.allclose(dt, np.diff(tlist)): raise ValueError("tlist must be evenly spaced.") - generator = self.PreSetWiener( + generator = PreSetWiener( noise, tlist, len(self.rhs.sc_ops), self.heterodyne, measurement ) + state0 = self._prepare_state(state) try: old_dt = None if "dt" in self._integrator.options: old_dt = self._integrator.options["dt"] self._integrator.options["dt"] = dt - result = self._run_inner_traj_loop(generator, state, tlist, e_ops) + mid_time = time() + result = self._run_inner_traj_loop(generator, state0, tlist, e_ops) except Exception as err: if old_dt is not None: self._integrator.options["dt"] = old_dt raise + stats['preparation time'] += mid_time - start_time + stats['run time'] = time() - mid_time + result.stats.update(stats) return result @classmethod diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index c6bf3cd95e..eae9c732ae 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -327,7 +327,7 @@ def test_m_ops(heterodyne): times = np.linspace(0, 1.0, 51) - options = {"store_measurement": True,} + options = {"store_measurement": "end",} solver = SMESolver(H, sc_ops, heterodyne=heterodyne, options=options) solver.m_ops = m_ops @@ -411,3 +411,91 @@ def test_small_step_warnings(method): qeye(2), basis(2), [0, 0.0000001], [qeye(2)], options={"method": method} ) + + +@pytest.mark.parametrize("method", ["euler", "platen"]) +@pytest.mark.parametrize("heterodyne", [True, False]) +def test_run_from_experiment_close(method, heterodyne): + N = 10 + + H = num(N) + a = destroy(N) + sc_ops = [a, a.dag() * 0.1] + psi0 = basis(N, N-1) + tlist = np.linspace(0, 1, 101) + options = { + "store_measurement": "start", + "dt": 0.01, + "store_states": True, + "method": method, + } + solver = SSESolver(H, sc_ops, heterodyne, options=options) + res_forward = solver.run(psi0, tlist, 1, e_ops=[H]) + res_backward = solver.run_from_experiment( + psi0, tlist, res_forward.dW[0], e_ops=[H] + ) + res_measure = solver.run_from_experiment( + psi0, tlist, res_forward.measurement[0], e_ops=[H], measurement=True + ) + + np.testing.assert_allclose( + res_backward.measurement, res_forward.measurement[0], atol=1e-10 + ) + np.testing.assert_allclose( + res_measure.measurement, res_forward.measurement[0], atol=1e-10 + ) + + np.testing.assert_allclose(res_backward.dW, res_forward.dW[0], atol=1e-10) + np.testing.assert_allclose(res_measure.dW, res_forward.dW[0], atol=1e-10) + + np.testing.assert_allclose( + res_backward.expect, res_forward.expect, atol=1e-10 + ) + np.testing.assert_allclose( + res_measure.expect, res_forward.expect, atol=1e-10 + ) + + +@pytest.mark.parametrize( + "method", ["euler", "milstein", "platen", "pred_corr"] +) +@pytest.mark.parametrize("heterodyne", [True, False]) +def test_run_from_experiment_open(method, heterodyne): + N = 10 + + H = num(N) + a = destroy(N) + sc_ops = [a, a.dag() * 0.1] + psi0 = basis(N, N-1) + tlist = np.linspace(0, 1, 101) + options = { + "store_measurement": "start", + "dt": 0.01, + "store_states": True, + "method": method, + } + solver = SMESolver(H, sc_ops, heterodyne, options=options) + res_forward = solver.run(psi0, tlist, 1, e_ops=[H]) + res_backward = solver.run_from_experiment( + psi0, tlist, res_forward.dW[0], e_ops=[H] + ) + res_measure = solver.run_from_experiment( + psi0, tlist, res_forward.measurement[0], e_ops=[H], measurement=True + ) + + np.testing.assert_allclose( + res_backward.measurement, res_forward.measurement[0], atol=1e-10 + ) + np.testing.assert_allclose( + res_measure.measurement, res_forward.measurement[0], atol=1e-10 + ) + + np.testing.assert_allclose(res_backward.dW, res_forward.dW[0], atol=1e-10) + np.testing.assert_allclose(res_measure.dW, res_forward.dW[0], atol=1e-10) + + np.testing.assert_allclose( + res_backward.expect, res_forward.expect, atol=1e-10 + ) + np.testing.assert_allclose( + res_measure.expect, res_forward.expect, atol=1e-10 + ) From a59383e3f8e80e3d559e3856d4b2c02368956c1d Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 6 Feb 2024 13:11:23 -0500 Subject: [PATCH 008/305] Fix options name in sode test --- qutip/tests/solver/test_sode_method.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/tests/solver/test_sode_method.py b/qutip/tests/solver/test_sode_method.py index 4b47fcf79f..5416f5335a 100644 --- a/qutip/tests/solver/test_sode_method.py +++ b/qutip/tests/solver/test_sode_method.py @@ -60,7 +60,7 @@ def _make_oper(kind, N): pytest.param("Euler", 0.5, {}, id="Euler"), pytest.param("Milstein", 1.0, {}, id="Milstein"), pytest.param("Milstein_imp", 1.0, {}, id="Milstein implicit"), - pytest.param("Milstein_imp", 1.0, {"solver_method": "inv"}, + pytest.param("Milstein_imp", 1.0, {"solve_method": "inv"}, id="Milstein implicit inv"), pytest.param("Platen", 1.0, {}, id="Platen"), pytest.param("PredCorr", 1.0, {}, id="PredCorr"), @@ -68,7 +68,7 @@ def _make_oper(kind, N): pytest.param("Taylor15", 1.5, {}, id="Taylor15"), pytest.param("Explicit15", 1.5, {}, id="Explicit15"), pytest.param("Taylor15_imp", 1.5, {}, id="Taylor15 implicit"), - pytest.param("Taylor15_imp", 1.5, {"solver_method": "inv"}, + pytest.param("Taylor15_imp", 1.5, {"solve_method": "inv"}, id="Taylor15 implicit inv"), ]) @pytest.mark.parametrize(['H', 'sc_ops'], [ @@ -79,7 +79,7 @@ def _make_oper(kind, N): pytest.param("qeye", ["qeye", "destroy", "destroy2"], id='3 sc_ops'), ]) def test_methods(H, sc_ops, method, order, kw): - if kw == {"solver_method": "inv"} and ("td" in H or "td" in sc_ops[0]): + if kw == {"solve_method": "inv"} and ("td" in H or "td" in sc_ops[0]): pytest.skip("inverse method only available for constant cases.") N = 5 H = _make_oper(H, N) From 82ecb639a16137449cb5bcd5d11792a2bd006bae Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 6 Feb 2024 13:13:34 -0500 Subject: [PATCH 009/305] Add towncrier --- doc/changes/2318.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/changes/2318.feature diff --git a/doc/changes/2318.feature b/doc/changes/2318.feature new file mode 100644 index 0000000000..d8b7f54b23 --- /dev/null +++ b/doc/changes/2318.feature @@ -0,0 +1,2 @@ +Create `run_from_experiment`, which allows to run stochastic evolution from +know noise or measurements. From 65f12d070367e2e8141932898df08e40965fdc22 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 6 Feb 2024 16:28:49 -0500 Subject: [PATCH 010/305] Fix pdf build --- qutip/solver/stochastic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 1ff8ff2cda..d1d0327e9d 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -698,10 +698,10 @@ def run_from_experiment( measurement : bool, default : False Whether the passed noise is the Wiener increments ``dW`` (gaussian - noise with standard derivation of dt**0.5), or the measurement: + noise with standard derivation of dt**0.5), or the measurement:: noise = dW/dt * dW_factors - + expect(sc_ops[i] + sc_ops[i].dag, state_t) + + expect(sc_ops[i] + sc_ops[i].dag, state_t) Note that the expectation value is usally computed at the start of the step. Only available for limited integration methods. From 5fb9298d824a0403aac55bf5554be4ff4da3360c Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 6 Feb 2024 16:50:37 -0500 Subject: [PATCH 011/305] Fix codeclimat formatting --- qutip/solver/sode/_noise.py | 4 +--- qutip/solver/stochastic.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index 1eb8a2e34a..c51c63e30f 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -87,9 +87,7 @@ def __init__(self, noise, tlist, n_sc_ops, heterodyne, is_measurement): self.noise /= 2**0.5 def _extend(self, N): - raise ValueError( - f"Requested time is outside the integration range. {N} > {self.noise.shape[0]}" - ) + raise ValueError("Requested time is outside the integration range.") class _Noise: diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index d1d0327e9d..de6ee44da5 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -667,9 +667,9 @@ def _run_one_traj(self, seed, state, tlist, e_ops): return seed, self._run_inner_traj_loop(generator, state, tlist, e_ops) def run_from_experiment( - self, state, tlist, noise, *, - args=None, e_ops=(), measurement=False, - ): + self, state, tlist, noise, *, + args=None, e_ops=(), measurement=False, + ): """ Run a single trajectory from a given state and noise. From f84989077c184b4c2f9670e6003f76ce90eb3d5a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 7 Feb 2024 09:53:02 -0500 Subject: [PATCH 012/305] Increase test stability --- qutip/tests/solver/test_stochastic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index eae9c732ae..d4eeaac416 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -422,10 +422,10 @@ def test_run_from_experiment_close(method, heterodyne): a = destroy(N) sc_ops = [a, a.dag() * 0.1] psi0 = basis(N, N-1) - tlist = np.linspace(0, 1, 101) + tlist = np.linspace(0, 0.1, 101) options = { "store_measurement": "start", - "dt": 0.01, + "dt": tlist[1], "store_states": True, "method": method, } From 94cc9c5a0564481bf49aca88dae67492b3259c38 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 12 Feb 2024 13:40:07 -0500 Subject: [PATCH 013/305] Start typehints --- qutip/core/coefficient.py | 28 +++++++++++++++++++++++++--- qutip/core/cy/qobjevo.pyx | 22 +++++++++++++++++----- qutip/solver/mesolve.py | 17 ++++++++++++++--- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index cf30326685..04ab77011e 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -1,4 +1,5 @@ import numpy as np +from numpy.typing import ArrayLike import scipy import scipy.interpolate import os @@ -10,6 +11,7 @@ import importlib import warnings import numbers +from typing import Union, Callable, Any from collections import defaultdict from setuptools import setup, Extension try: @@ -39,6 +41,17 @@ def _return(base, **kwargs): return base +CoefficientLike = Union[ + Coefficient, + str, + Callable[[float, ...], complex], + np.ndarray, + scipy.interpolate.PPoly, + scipy.interpolate.BSpline, + Any, +] + + # The `coefficient` function is dispatcher for the type of the `base` to the # function that created the `Coefficient` object. `coefficient_builders` stores # the map `type -> function(base, **kw)`. Optional module can add their @@ -51,9 +64,18 @@ def _return(base, **kwargs): } -def coefficient(base, *, tlist=None, args={}, args_ctypes={}, - order=3, compile_opt=None, function_style=None, - boundary_conditions=None, **kwargs): +def coefficient( + base: CoefficientLike, + *, + tlist: ArrayLike = None, + args: dict = {}, + args_ctypes: dict = {}, + order: int = 3, + compile_opt: dict = None, + function_style: str = None, + boundary_conditions: tuple | str = None, + **kwargs +): """Build ``Coefficient`` for time dependent systems: ``` diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index c2b4d5d3ae..bc4d3c05bb 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -5,12 +5,12 @@ import numpy as np import numbers import itertools from functools import partial - +from typing import Union, Callable, Tuple, List import qutip from .. import Qobj from .. import data as _data from ..dimensions import Dimensions -from ..coefficient import coefficient, CompilationOptions +from ..coefficient import coefficient, CompilationOptions, CoefficientLike from ._element import * from qutip.settings import settings @@ -186,9 +186,18 @@ cdef class QobjEvo: qevo = H0 + H1 * coeff """ - def __init__(QobjEvo self, Q_object, args=None, *, copy=True, compress=True, - function_style=None, - tlist=None, order=3, boundary_conditions=None): + def __init__( + QobjEvo self, + Q_object: QobjEvoLike, + args: dict = None, + *, + copy: bool = True, + compress: bool = True, + function_style: str = None, + tlist: ArrayLike = None, + order: int = 3, + boundary_conditions: tuple | str = None, + ): if isinstance(Q_object, QobjEvo): self._dims = Q_object._dims self.shape = Q_object.shape @@ -1108,3 +1117,6 @@ class _Feedback: Raise an error when the dims of the e_ops / state don't match. Tell the dims to the feedback for reconstructing the Qobj. """ + +Element = Union[Callable[[float, ...], Qobj], Qobj, Tuple[Qobj, CoefficientLike]] +QobjEvoLike = Union[Qobj, QobjEvo, Element, List[Element]] diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index caafc5ef1c..c498aa6488 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -6,8 +6,11 @@ __all__ = ['mesolve', 'MESolver'] import numpy as np +from numpy.typing import ArrayLike +from typing import Any, Iterable, Mapping from time import time from .. import (Qobj, QobjEvo, isket, liouvillian, ket2dm, lindblad_dissipator) +from ..core.cy.qobjevo import QobjEvoLike from ..core import stack_columns, unstack_columns from ..core.data import to from .solver_base import Solver, _solver_deprecation @@ -15,8 +18,16 @@ from ._feedback import _QobjFeedback, _DataFeedback -def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None, - **kwargs): +def mesolve( + H: QobjEvoLike, + rho0: Qobj, + tlist: ArrayLike, + c_ops: QobjEvoLike | Iterable[QobjEvoLike] = None, + e_ops: QobjEvoLike | Mapping[Any, QobjEvoLike] = None, + args: dict[str, Any] = None, + options: dict = None, + **kwargs +): """ Master equation evolution of a density matrix for a given Hamiltonian and set of collapse operators, or a Liouvillian. @@ -192,7 +203,7 @@ class MESolver(SESolver): 'method': 'adams', } - def __init__(self, H, c_ops=None, *, options=None): + def __init__(self, H: Qobj | QobjEvo, c_ops: list[Qobj | QobjEvo] = None, *, options: dict=None): _time_start = time() if not isinstance(H, (Qobj, QobjEvo)): From db6f4dc581b0387f927a31b688930a0ec862a567 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 13:53:32 -0500 Subject: [PATCH 014/305] types for mesolve --- qutip/core/cy/qobjevo.pyi | 71 +++++++++++++++++++++++++++++++++++++++ qutip/core/cy/qobjevo.pyx | 6 ++-- qutip/solver/mesolve.py | 8 ++--- qutip/typing.py | 9 +++++ 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 qutip/core/cy/qobjevo.pyi create mode 100644 qutip/typing.py diff --git a/qutip/core/cy/qobjevo.pyi b/qutip/core/cy/qobjevo.pyi new file mode 100644 index 0000000000..7eb1c0b698 --- /dev/null +++ b/qutip/core/cy/qobjevo.pyi @@ -0,0 +1,71 @@ +from qutip.typing import LayerType +from qutip.core.qobj import Qobj +from qutip.core.data import Data +from qutip.core.dimensions import Dimensions +from qutip.core.coeffient import Coefficient +from typing import Any, ClassVar, overload, Callable, Dict, Tuple, List, Union + +Element = Union[Callable[[float, ...], Qobj], Qobj, Tuple[Qobj, CoefficientLike]] +QobjEvoLike = Union[Qobj, QobjEvo, Element, List[Element]] + +class QobjEvo: + dims: list + isbra: bool + isconstant: bool + isket: bool + isoper: bool + isoperbra: bool + isoperket: bool + issuper: bool + num_elements: int + shape: Tuple[int, int] + superrep: str + type: str + def __init__( + self, + Q_object: QobjEvoLike, + args: Dict[str, Any] = None, + *, + copy: bool = True, + compress: bool = True, + function_style: str = None, + tlist: ArrayLike = None, + order: int = 3, + boundary_conditions: Union[tuple, str] = None, + ) -> None: ... + @overload + def arguments(self, new_args: Dict[str, Any]) -> None: ... + @overload + def arguments(self, **new_args) -> None: ... + def compress(self) -> QobjEvo: ... + def tidyup(self, atol: Number) -> QobjEvo: ... + def copy(self) -> QobjEvo: ... + def conj(self) -> QobjEvo: ... + def dag(self) -> QobjEvo: ... + def trans(self) -> QobjEvo: ... + def to(self, data_type: LayerType) -> QobjEvo: ... + def linear_map(self, op_mapping: Callable[[Qobj], Qobj]) -> QobjEvo: ... + def expect(self, t: Number, state: Qobj, check_real: bool = True) -> Number: ... + def expect_data(self, t: Number, state: Data) -> Number: ... + def matmul(self, t: Number, state: Qobj) -> Qobj: ... + def matmul_data(self, t: Number, state: Data, out: Data = None) -> Data: ... + def to_list(self) -> List[Element]: ... + def __add__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... + def __iadd__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... + def __radd__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... + def __sub__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... + def __isub__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... + def __rsub__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... + def __and__(self, other: Union[Number, Qobj]) -> QobjEvo: ... + def __rand__(self, other: Union[Number, Qobj]) -> QobjEvo: ... + def __call__(self, t: Number, **new_args) -> Qobj: ... + def __matmul__(self, other: Union[QobjEvo, Qobj]) -> QobjEvo: ... + def __imatmul__(self, other: Union[QobjEvo, Qobj]) -> QobjEvo: ... + def __rmatmul__(self, other: Union[QobjEvo, Qobj]) -> QobjEvo: ... + def __mul__(self, other: Union[Number, Coefficient]) -> QobjEvo: ... + def __imul__(self, other: Union[Number, Coefficient]) -> QobjEvo: ... + def __rmul__(self, other: Union[Number, Coefficient]) -> QobjEvo: ... + def __truediv__(self, other : Number) -> QobjEvo: ... + def __idiv__(self, other : Number) -> QobjEvo: ... + def __neg__(self) -> QobjEvo: ... + def __reduce__(self): ... diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index bc4d3c05bb..7c4ef6bd5e 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -5,7 +5,7 @@ import numpy as np import numbers import itertools from functools import partial -from typing import Union, Callable, Tuple, List +from typing import Union, Callable, Tuple, List, Dict import qutip from .. import Qobj from .. import data as _data @@ -189,14 +189,14 @@ cdef class QobjEvo: def __init__( QobjEvo self, Q_object: QobjEvoLike, - args: dict = None, + args: Dict[str, Any] = None, *, copy: bool = True, compress: bool = True, function_style: str = None, tlist: ArrayLike = None, order: int = 3, - boundary_conditions: tuple | str = None, + boundary_conditions: Union[tuple, str] = None, ): if isinstance(Q_object, QobjEvo): self._dims = Q_object._dims diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index c498aa6488..8ecee736c3 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -7,7 +7,7 @@ import numpy as np from numpy.typing import ArrayLike -from typing import Any, Iterable, Mapping +from typing import Any, Iterable, Mapping, Sequence from time import time from .. import (Qobj, QobjEvo, isket, liouvillian, ket2dm, lindblad_dissipator) from ..core.cy.qobjevo import QobjEvoLike @@ -142,7 +142,7 @@ def mesolve( H = QobjEvo(H, args=args, tlist=tlist) c_ops = c_ops if c_ops is not None else [] - if not isinstance(c_ops, (list, tuple)): + if not isinstance(c_ops, Iterable): c_ops = [c_ops] c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] @@ -193,7 +193,7 @@ class MESolver(SESolver): Diverse diagnostic statistics of the evolution. """ name = "mesolve" - _avail_integrators = {} + _avail_integrators: dict[str, object] = {} solver_options = { "progress_bar": "", "progress_kwargs": {"chunk_size":10}, @@ -203,7 +203,7 @@ class MESolver(SESolver): 'method': 'adams', } - def __init__(self, H: Qobj | QobjEvo, c_ops: list[Qobj | QobjEvo] = None, *, options: dict=None): + def __init__(self, H: Qobj | QobjEvo, c_ops: Qobj | QobjEvo | Sequence[Qobj | QobjEvo] = None, *, options: dict=None): _time_start = time() if not isinstance(H, (Qobj, QobjEvo)): diff --git a/qutip/typing.py b/qutip/typing.py new file mode 100644 index 0000000000..fe2715520b --- /dev/null +++ b/qutip/typing.py @@ -0,0 +1,9 @@ +import typing + +from .core.cy.qobjEvo import QobjEvoLike, Element +from .core.coeffients import CoefficientLike + +__all__ = ["QobjEvoLike"] + + +LayerType = Union[str, type] From 8f26cd44534d9963317090ec03c0650e5a5a4b47 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 16:17:45 -0500 Subject: [PATCH 015/305] sesolve and start of result --- qutip/solver/mesolve.py | 30 +++++++++++++++++++++--------- qutip/solver/result.py | 6 ++++++ qutip/solver/sesolve.py | 31 ++++++++++++++++++++++++++----- qutip/solver/solver_base.py | 32 ++++++++++++++++++++++++-------- 4 files changed, 77 insertions(+), 22 deletions(-) diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index 8ecee736c3..6b1f1b5d38 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -7,27 +7,28 @@ import numpy as np from numpy.typing import ArrayLike -from typing import Any, Iterable, Mapping, Sequence +from typing import Any, Callable from time import time from .. import (Qobj, QobjEvo, isket, liouvillian, ket2dm, lindblad_dissipator) from ..core.cy.qobjevo import QobjEvoLike from ..core import stack_columns, unstack_columns -from ..core.data import to +from ..core import data as _data from .solver_base import Solver, _solver_deprecation from .sesolve import sesolve, SESolver from ._feedback import _QobjFeedback, _DataFeedback +from . import Result def mesolve( H: QobjEvoLike, rho0: Qobj, tlist: ArrayLike, - c_ops: QobjEvoLike | Iterable[QobjEvoLike] = None, - e_ops: QobjEvoLike | Mapping[Any, QobjEvoLike] = None, + c_ops: QobjEvoLike | list[QobjEvoLike] = None, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, args: dict[str, Any] = None, - options: dict = None, + options: dict[str, Any] = None, **kwargs -): +) -> Result: """ Master equation evolution of a density matrix for a given Hamiltonian and set of collapse operators, or a Liouvillian. @@ -142,7 +143,7 @@ def mesolve( H = QobjEvo(H, args=args, tlist=tlist) c_ops = c_ops if c_ops is not None else [] - if not isinstance(c_ops, Iterable): + if not isinstance(c_ops, (list, tuple)): c_ops = [c_ops] c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] @@ -203,7 +204,13 @@ class MESolver(SESolver): 'method': 'adams', } - def __init__(self, H: Qobj | QobjEvo, c_ops: Qobj | QobjEvo | Sequence[Qobj | QobjEvo] = None, *, options: dict=None): + def __init__( + self, + H: Qobj | QobjEvo, + c_ops: Qobj | QobjEvo | list[Qobj | QobjEvo] = None, + *, + options: dict=None + ): _time_start = time() if not isinstance(H, (Qobj, QobjEvo)): @@ -231,7 +238,12 @@ def _initialize_stats(self): return stats @classmethod - def StateFeedback(cls, default=None, raw_data=False, prop=False): + def StateFeedback( + cls, + default: Qobj | _data.Data = None, + raw_data: bool = False, + prop: bool = False + ): """ State of the evolution to be used in a time-dependent operator. diff --git a/qutip/solver/result.py b/qutip/solver/result.py index b1d1bf2bbd..72eafdd1ec 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -199,6 +199,12 @@ class Result(_BaseResult): options : dict The options for this result class. """ + + times: list[float] + states: list[Qobj] + final_state : Qobj + + def __init__(self, e_ops, options, *, solver=None, stats=None, **kw): super().__init__(options, solver=solver, stats=stats) raw_ops = self._e_ops_to_dict(e_ops) diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 8d43988299..54d7534a9d 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -5,13 +5,29 @@ __all__ = ['sesolve', 'SESolver'] import numpy as np +from numpy.typing import ArrayLike from time import time +from typing import Any, Callable from .. import Qobj, QobjEvo +from ..core import data as _data +from ..core.cy.qobjevo import QobjEvoLike from .solver_base import Solver, _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback +from . import Result -def sesolve(H, psi0, tlist, e_ops=None, args=None, options=None, **kwargs): +E_opType = Qobj | QobjEvo | Callable[[float, Qobj], Any] + + +def sesolve( + H: QobjEvoLike, + psi0: Qobj, + tlist: ArrayLike, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + **kwargs +) -> Result: """ Schrodinger equation evolution of a state vector or unitary matrix for a given Hamiltonian. @@ -138,7 +154,7 @@ class SESolver(Solver): 'method': 'adams', } - def __init__(self, H, *, options=None): + def __init__(self, H: Qobj | QobjEvo, *, options: dict[str, Any] = None): _time_start = time() if not isinstance(H, (Qobj, QobjEvo)): @@ -157,7 +173,7 @@ def _initialize_stats(self): return stats @property - def options(self): + def options(self) -> dict: """ Solver's options: @@ -188,11 +204,16 @@ def options(self): return self._options @options.setter - def options(self, new_options): + def options(self, new_options: dict[str, Any]): Solver.options.fset(self, new_options) @classmethod - def StateFeedback(cls, default=None, raw_data=False, prop=False): + def StateFeedback( + cls, + default: Qobj | _data.Data = None, + raw_data: bool = False, + prop: bool = False + ): """ State of the evolution to be used in a time-dependent operator. diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 28a4c8ba60..5ba7e36bc9 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -1,5 +1,8 @@ __all__ = ['Solver'] +from numpy.typing import ArrayLike +from numbers import Number +from typing import Any, Callable from .. import Qobj, QobjEvo, ket2dm from .options import _SolverOptions from ..core import stack_columns, unstack_columns @@ -106,7 +109,14 @@ def _restore_state(self, data, *, copy=True): return state - def run(self, state0, tlist, *, args=None, e_ops=None): + def run( + self, + state0: Qobj, + tlist: ArrayLike, + *, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + args: dict[str, Any] = None, + ) -> _resultclass: """ Do the evolution of the Quantum system. @@ -126,10 +136,10 @@ def run(self, state0, tlist, *, args=None, e_ops=None): evolution. Each times of the list must be increasing, but does not need to be uniformy distributed. - args : dict, optional {None} + args : dict, optional Change the ``args`` of the rhs for the evolution. - e_ops : list {None} + e_ops : list, optional List of Qobj, QobjEvo or callable to compute the expectation values. Function[s] must have the signature f(t : float, state : Qobj) -> expect. @@ -165,7 +175,7 @@ def run(self, state0, tlist, *, args=None, e_ops=None): # stats.update(_integrator.stats) return results - def start(self, state0, t0): + def start(self, state0: Qobj, t0: Number) -> None: """ Set the initial state and time for a step evolution. @@ -181,7 +191,13 @@ def start(self, state0, t0): self._integrator.set_state(t0, self._prepare_state(state0)) self.stats["preparation time"] += time() - _time_start - def step(self, t, *, args=None, copy=True): + def step( + self, + t: Number, + *, + args: dict[str, Any] = None, + copy: bool = True + ) -> Qobj: """ Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. @@ -238,7 +254,7 @@ def sys_dims(self): return self.rhs.dims[0] @property - def options(self): + def options(self) -> dict[str, Any]: """ method: str Which ordinary differential equation integration method to use. @@ -278,7 +294,7 @@ def _parse_options(self, new_options, default, old_options): return included_options, extra_options @options.setter - def options(self, new_options): + def options(self, new_options: dict[str, Any]): if not hasattr(self, "_options"): self._options = {} if new_options is None: @@ -399,7 +415,7 @@ def add_integrator(cls, integrator, key): cls._avail_integrators[key] = integrator @classmethod - def ExpectFeedback(cls, operator, default=0.): + def ExpectFeedback(cls, operator: Qobj | QobjEvo, default: Any = 0.): """ Expectation value of the instantaneous state of the evolution to be used by a time-dependent operator. From d661cba9614ff0df0f7acbc66f0af57cc65cce60 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 16:44:10 -0500 Subject: [PATCH 016/305] Add types to result --- qutip/solver/result.py | 53 ++++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 5dfc622b47..9dc11e08eb 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1,7 +1,9 @@ """ Class for solve function results""" -from typing import TypedDict +from typing import TypedDict, Any import numpy as np +from numbers import Number +from numpy.typing import ArrayLike from ..core import Qobj, QobjEvo, expect, isket, ket2dm, qzero_like __all__ = [ @@ -130,7 +132,7 @@ def add_processor(self, f, requires_copy=False): class ResultOptions(TypedDict): - store_states: bool + store_states: bool | None store_final_state: bool @@ -218,19 +220,18 @@ class Result(_BaseResult): times: list[float] states: list[Qobj] - final_state : Qobj options: ResultOptions + e_data : dict[Any, list[Any]] def __init__( self, e_ops, options: ResultOptions, *, - solver=None, - stats=None, + solver: str = None, + stats: dict[str, Any] = None, **kw, ): - super().__init__(options, solver=solver, stats=stats) raw_ops = self._e_ops_to_dict(e_ops) self.e_data = {k: [] for k in raw_ops} @@ -359,11 +360,11 @@ def __repr__(self): return "\n".join(lines) @property - def expect(self): + def expect(self) -> list[ArrayLike]: return [np.array(e_op) for e_op in self.e_data.values()] @property - def final_state(self): + def final_state(self) -> Qobj: if self._final_state is not None: return self._final_state if self.states: @@ -372,7 +373,7 @@ def final_state(self): class MultiTrajResultOptions(TypedDict): - store_states: bool + store_states: bool | None store_final_state: bool keep_runs_results: bool @@ -499,6 +500,12 @@ class MultiTrajResult(_BaseResult): """ options: MultiTrajResultOptions + trajectories: list[Result] + num_trajectories: int + seeds: list + runs_e_data: dict[Any, Number] + std_e_data: dict[Any, Number] + average_e_data: dict[Any, Number] def __init__( self, @@ -766,7 +773,7 @@ def add_end_condition(self, ntraj, target_tol=None): self._early_finish_check = self._target_tolerance_end @property - def runs_states(self): + def runs_states(self) -> list[list[Qobj]]: """ States of every runs as ``states[run][t]``. """ @@ -776,7 +783,7 @@ def runs_states(self): return None @property - def average_states(self): + def average_states(self) -> list[Qobj]: """ States averages as density matrices. """ @@ -802,7 +809,7 @@ def states(self): return self.runs_states or self.average_states @property - def runs_final_states(self): + def runs_final_states(self) -> list[Qobj]: """ Last states of each trajectories. """ @@ -812,7 +819,7 @@ def runs_final_states(self): return None @property - def average_final_state(self): + def average_final_state(self) -> Qobj: """ Last states of each trajectories averaged into a density matrix. """ @@ -830,26 +837,26 @@ def final_state(self): return self.runs_final_states or self.average_final_state @property - def average_expect(self): + def average_expect(self) -> list[ArrayLike]: return [np.array(val) for val in self.average_e_data.values()] @property - def std_expect(self): + def std_expect(self) -> list[ArrayLike]: return [np.array(val) for val in self.std_e_data.values()] @property - def runs_expect(self): + def runs_expect(self) -> list[ArrayLike]: return [np.array(val) for val in self.runs_e_data.values()] @property - def expect(self): + def expect(self) -> list[ArrayLike]: return [np.array(val) for val in self.e_data.values()] @property - def e_data(self): + def e_data(self) -> list[ArrayLike]: return self.runs_e_data or self.average_e_data - def steady_state(self, N=0): + def steady_state(self, N=0) -> Qobj: """ Average the states of the last ``N`` times of every runs as a density matrix. Should converge to the steady state in the right circumstances. @@ -1022,7 +1029,7 @@ def _post_init(self): self.add_processor(self._add_collapse) @property - def col_times(self): + def col_times(self) -> list[list[float]]: """ List of the times of the collapses for each runs. """ @@ -1034,7 +1041,7 @@ def col_times(self): return out @property - def col_which(self): + def col_which(self) -> list[list[int]]: """ List of the indexes of the collapses for each runs. """ @@ -1046,7 +1053,7 @@ def col_which(self): return out @property - def photocurrent(self): + def photocurrent(self) -> list[ArrayLike]: """ Average photocurrent or measurement of the evolution. """ @@ -1064,7 +1071,7 @@ def photocurrent(self): return mesurement @property - def runs_photocurrent(self): + def runs_photocurrent(self) -> list[list[ArrayLike]]: """ Photocurrent or measurement of each runs. """ From 3f0a35d5bc604b5fd35610aad6b490190d7aaf05 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 14 Feb 2024 17:13:00 -0500 Subject: [PATCH 017/305] fixing --- qutip/core/cy/qobjevo.pyi | 17 ++++++++++++----- qutip/core/cy/qobjevo.pyx | 8 +++++++- qutip/core/qobj.py | 14 +++++++------- qutip/solver/result.py | 2 +- qutip/solver/sesolve.py | 3 --- qutip/solver/solver_base.py | 2 +- 6 files changed, 28 insertions(+), 18 deletions(-) diff --git a/qutip/core/cy/qobjevo.pyi b/qutip/core/cy/qobjevo.pyi index 7eb1c0b698..5cd6e56b62 100644 --- a/qutip/core/cy/qobjevo.pyi +++ b/qutip/core/cy/qobjevo.pyi @@ -1,12 +1,19 @@ from qutip.typing import LayerType from qutip.core.qobj import Qobj from qutip.core.data import Data -from qutip.core.dimensions import Dimensions -from qutip.core.coeffient import Coefficient -from typing import Any, ClassVar, overload, Callable, Dict, Tuple, List, Union +from qutip.core.coefficient import Coefficient, CoefficientLike +from numbers import Number +from typing import Any, overload, Callable, Dict, Tuple, List, Union, Sequence +from typing_extensions import Protocol -Element = Union[Callable[[float, ...], Qobj], Qobj, Tuple[Qobj, CoefficientLike]] -QobjEvoLike = Union[Qobj, QobjEvo, Element, List[Element]] + +class QEvoFunction(Protocol): + def __call__(self, t: Number, **kwargs) -> Qobj: + ... + + +Element = Union[QEvoFunction, Qobj, Sequence[Qobj, CoefficientLike]] +QobjEvoLike = Union[Qobj, QobjEvo, Element, Sequence[Element]] class QobjEvo: dims: list diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index 7c4ef6bd5e..6766c2dec1 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -6,6 +6,7 @@ import numbers import itertools from functools import partial from typing import Union, Callable, Tuple, List, Dict +from typing_extensions import Protocol import qutip from .. import Qobj from .. import data as _data @@ -1118,5 +1119,10 @@ class _Feedback: Tell the dims to the feedback for reconstructing the Qobj. """ -Element = Union[Callable[[float, ...], Qobj], Qobj, Tuple[Qobj, CoefficientLike]] + +class QEvoFunction(Protocol): + def __call__(self, t: Number, **kwargs) -> Qobj: + ... + +Element = Union[QEvoFunction, Qobj, Sequence[Qobj, CoefficientLike]] QobjEvoLike = Union[Qobj, QobjEvo, Element, List[Element]] diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index bdc72ca67e..12ebff49a8 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -39,37 +39,37 @@ } -def isbra(x): +def isbra(x: Qobj | "QobjEvo"): from .cy.qobjevo import QobjEvo return isinstance(x, (Qobj, QobjEvo)) and x.type in ['bra', 'scalar'] -def isket(x): +def isket(x: Qobj | "QobjEvo"): from .cy.qobjevo import QobjEvo return isinstance(x, (Qobj, QobjEvo)) and x.type in ['ket', 'scalar'] -def isoper(x): +def isoper(x: Qobj | "QobjEvo"): from .cy.qobjevo import QobjEvo return isinstance(x, (Qobj, QobjEvo)) and x.type in ['oper', 'scalar'] -def isoperbra(x): +def isoperbra(x: Qobj | "QobjEvo"): from .cy.qobjevo import QobjEvo return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-bra'] -def isoperket(x): +def isoperket(x: Qobj | "QobjEvo"): from .cy.qobjevo import QobjEvo return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-ket'] -def issuper(x): +def issuper(x: Qobj | "QobjEvo"): from .cy.qobjevo import QobjEvo return isinstance(x, (Qobj, QobjEvo)) and x.type in ['super'] -def isherm(x): +def isherm(x: Qobj | "QobjEvo"): return isinstance(x, Qobj) and x.isherm diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 9dc11e08eb..7612441efb 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -2,8 +2,8 @@ from typing import TypedDict, Any import numpy as np -from numbers import Number from numpy.typing import ArrayLike +from numbers import Number from ..core import Qobj, QobjEvo, expect, isket, ket2dm, qzero_like __all__ = [ diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 54d7534a9d..5b48394bc1 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -16,9 +16,6 @@ from . import Result -E_opType = Qobj | QobjEvo | Callable[[float, Qobj], Any] - - def sesolve( H: QobjEvoLike, psi0: Qobj, diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 5ba7e36bc9..839bffb40c 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -116,7 +116,7 @@ def run( *, e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, args: dict[str, Any] = None, - ) -> _resultclass: + ) -> Result: """ Do the evolution of the Quantum system. From fe3c7f22c8dc33fa2a3e2ef821d9068fc300a432 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 15 Feb 2024 10:26:23 -0500 Subject: [PATCH 018/305] Type in Qobj --- qutip/core/__init__.py | 1 + qutip/core/properties.py | 35 ++++++ qutip/core/qobj.py | 235 +++++++++++++++++++++------------------ 3 files changed, 160 insertions(+), 111 deletions(-) create mode 100644 qutip/core/properties.py diff --git a/qutip/core/__init__.py b/qutip/core/__init__.py index 4574f20454..296f26fa40 100644 --- a/qutip/core/__init__.py +++ b/qutip/core/__init__.py @@ -12,6 +12,7 @@ from .subsystem_apply import * from .blochredfield import * from .energy_restricted import * +from .properties import * from . import gates del cy # File in cy are not public facing diff --git a/qutip/core/properties.py b/qutip/core/properties.py new file mode 100644 index 0000000000..47b29cc968 --- /dev/null +++ b/qutip/core/properties.py @@ -0,0 +1,35 @@ +from . import Qobj, QobjEvo + +__all__ = [ + 'isbra', 'isket', 'isoper', 'issuper', 'isoperbra', 'isoperket', 'isherm' +] + + +def isbra(x: Qobj | QobjEvo): + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['bra', 'scalar'] + + +def isket(x: Qobj | QobjEvo): + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['ket', 'scalar'] + + +def isoper(x: Qobj | QobjEvo): + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['oper', 'scalar'] + + +def isoperbra(x: Qobj | QobjEvo): + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-bra'] + + +def isoperket(x: Qobj | QobjEvo): + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-ket'] + + +def issuper(x: Qobj | QobjEvo): + return isinstance(x, (Qobj, QobjEvo)) and x.type in ['super'] + + +def isherm(x: Qobj): + if not isinstance(x, Qobj): + raise TypeError(f"Invalid input type, got {type(x)}, exected Qobj") + return x.isherm \ No newline at end of file diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 12ebff49a8..2f470d5af5 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -2,21 +2,20 @@ operators, and related functions. """ -__all__ = [ - 'Qobj', 'isbra', 'isket', 'isoper', 'issuper', 'isoperbra', 'isoperket', - 'isherm', 'ptrace', -] +__all__ = ['Qobj', 'ptrace',] import functools import numbers import warnings - +from typing import Any, Literal import numpy as np +from numpy.typing import ArrayLike import scipy.sparse from .. import __version__ from ..settings import settings from . import data as _data +from qutip.typing import LayerType from .dimensions import ( enumerate_flat, collapse_dims_super, flatten, unflatten, Dimensions ) @@ -39,40 +38,6 @@ } -def isbra(x: Qobj | "QobjEvo"): - from .cy.qobjevo import QobjEvo - return isinstance(x, (Qobj, QobjEvo)) and x.type in ['bra', 'scalar'] - - -def isket(x: Qobj | "QobjEvo"): - from .cy.qobjevo import QobjEvo - return isinstance(x, (Qobj, QobjEvo)) and x.type in ['ket', 'scalar'] - - -def isoper(x: Qobj | "QobjEvo"): - from .cy.qobjevo import QobjEvo - return isinstance(x, (Qobj, QobjEvo)) and x.type in ['oper', 'scalar'] - - -def isoperbra(x: Qobj | "QobjEvo"): - from .cy.qobjevo import QobjEvo - return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-bra'] - - -def isoperket(x: Qobj | "QobjEvo"): - from .cy.qobjevo import QobjEvo - return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-ket'] - - -def issuper(x: Qobj | "QobjEvo"): - from .cy.qobjevo import QobjEvo - return isinstance(x, (Qobj, QobjEvo)) and x.type in ['super'] - - -def isherm(x: Qobj | "QobjEvo"): - return isinstance(x, Qobj) and x.isherm - - def _require_equal_type(method): """ Decorate a binary Qobj method to ensure both operands are Qobj and of the @@ -285,8 +250,15 @@ def _initialize_data(self, arg, dims, copy): raise ValueError('Provided dimensions do not match the data: ' + f"{self._dims.shape} vs {self._data.shape}") - def __init__(self, arg=None, dims=None, - copy=True, superrep=None, isherm=None, isunitary=None): + def __init__( + self, + arg: ArrayLike | Any =None, + dims: list[list[int]] | list[list[list[int]]] | Dimensions = None, + copy: bool = True, + superrep: str = None, + isherm: bool = None, + isunitary: bool = None + ): self._isherm = isherm self._isunitary = isunitary self._initialize_data(arg, dims, copy) @@ -294,7 +266,7 @@ def __init__(self, arg=None, dims=None, if superrep is not None: self.superrep = superrep - def copy(self): + def copy(self) -> Qobj: """Create identical copy""" return Qobj(arg=self._data, dims=self.dims, @@ -303,11 +275,11 @@ def copy(self): copy=True) @property - def dims(self): + def dims(self) -> list[list[int]] | list[list[list[int]]]: return self._dims.as_list() @dims.setter - def dims(self, dims): + def dims(self, dims: list[list[int]] | list[list[list[int]]] | Dimensions): dims = Dimensions(dims, rep=self.superrep) if dims.shape != self._data.shape: raise ValueError('Provided dimensions do not match the data: ' + @@ -315,23 +287,23 @@ def dims(self, dims): self._dims = dims @property - def type(self): + def type(self) -> str: return self._dims.type @property - def superrep(self): + def superrep(self) -> str: return self._dims.superrep @superrep.setter - def superrep(self, super_rep): + def superrep(self, super_rep: str): self._dims = self._dims.replace_superrep(super_rep) @property - def data(self): + def data(self) -> _data.Data: return self._data @data.setter - def data(self, data): + def data(self, data: _data.Data): if not isinstance(data, _data.Data): raise TypeError('Qobj data must be a data-layer format.') if self._dims.shape != data.shape: @@ -339,7 +311,7 @@ def data(self, data): f"{dims.shape} vs {self._data.shape}") self._data = data - def to(self, data_type): + def to(self, LayerType) -> Qobj: """ Convert the underlying data store of this `Qobj` into a different storage representation. @@ -381,7 +353,7 @@ def to(self, data_type): copy=False) @_require_equal_type - def __add__(self, other): + def __add__(self, other: Qobj | numbers.Number) -> Qobj: if other == 0: return self.copy() return Qobj(_data.add(self._data, other._data), @@ -389,11 +361,11 @@ def __add__(self, other): isherm=(self._isherm and other._isherm) or None, copy=False) - def __radd__(self, other): + def __radd__(self, other: Qobj | numbers.Number) -> Qobj: return self.__add__(other) @_require_equal_type - def __sub__(self, other): + def __sub__(self, other: Qobj | numbers.Number) -> Qobj: if other == 0: return self.copy() return Qobj(_data.sub(self._data, other._data), @@ -401,10 +373,10 @@ def __sub__(self, other): isherm=(self._isherm and other._isherm) or None, copy=False) - def __rsub__(self, other): + def __rsub__(self, other: Qobj | numbers.Number) -> Qobj: return self.__neg__().__add__(other) - def __mul__(self, other): + def __mul__(self, other: numbers.Number) -> Qobj: """ If other is a Qobj, we dispatch to __matmul__. If not, we check that other is a valid complex scalar, i.e., we can do @@ -438,12 +410,12 @@ def __mul__(self, other): isunitary=isunitary, copy=False) - def __rmul__(self, other): + def __rmul__(self, other: numbers.Number) -> Qobj: # Shouldn't be here unless `other.__mul__` has already been tried, so # we _shouldn't_ check that `other` is `Qobj`. return self.__mul__(other) - def __matmul__(self, other): + def __matmul__(self, other: Qobj) -> Qobj: if not isinstance(other, Qobj): try: other = Qobj(other) @@ -460,10 +432,10 @@ def __matmul__(self, other): copy=False ) - def __truediv__(self, other): + def __truediv__(self, other: numbers.Number) -> Qobj: return self.__mul__(1 / other) - def __neg__(self): + def __neg__(self) -> Qobj: return Qobj(_data.neg(self._data), dims=self._dims, isherm=self._isherm, @@ -486,7 +458,7 @@ def __getitem__(self, ind): pass return data.to_array()[ind] - def __eq__(self, other): + def __eq__(self, other) -> bool: if self is other: return True if not isinstance(other, Qobj) or self._dims != other._dims: @@ -494,7 +466,7 @@ def __eq__(self, other): return _data.iszero(_data.sub(self._data, other._data), tol=settings.core['atol']) - def __pow__(self, n, m=None): # calculates powers of Qobj + def __pow__(self, n: int, m=None) -> Qobj: # calculates powers of Qobj if ( self.type not in ('oper', 'super') or self._dims[0] != self._dims[1] @@ -540,7 +512,7 @@ def __repr__(self): # so we simply return the informal __str__ representation instead.) return self.__str__() - def __call__(self, other): + def __call__(self, other: Qobj) -> Qobj: """ Acts this Qobj on another Qobj either by left-multiplication, or by vectorization and devectorization, as @@ -595,14 +567,14 @@ def _repr_latex_(self): data += r'\end{array}\right)$$' return self._str_header() + data - def __and__(self, other): + def __and__(self, other: Qobj) -> Qobj: """ Syntax shortcut for tensor: A & B ==> tensor(A, B) """ return tensor(self, other) - def dag(self): + def dag(self) -> Qobj: """Get the Hermitian adjoint of the quantum object.""" if self._isherm: return self.copy() @@ -612,7 +584,7 @@ def dag(self): isunitary=self._isunitary, copy=False) - def conj(self): + def conj(self) -> Qobj: """Get the element-wise conjugation of the quantum object.""" return Qobj(_data.conj(self._data), dims=self._dims, @@ -620,7 +592,7 @@ def conj(self): isunitary=self._isunitary, copy=False) - def trans(self): + def trans(self) -> Qobj: """Get the matrix transpose of the quantum operator. Returns @@ -634,7 +606,7 @@ def trans(self): isunitary=self._isunitary, copy=False) - def dual_chan(self): + def dual_chan(self) -> Qobj: """Dual channel of quantum object representing a completely positive map. """ @@ -651,7 +623,11 @@ def dual_chan(self): J_dual.superrep = 'choi' return J_dual - def norm(self, norm=None, kwargs=None): + def norm( + self, + norm: Litteral["l2", "max", "fro", "tr", "one"] = None, + kwargs: dict[str, Any] = None + ) -> numbers.Number: """ Norm of a quantum object. @@ -689,7 +665,7 @@ def norm(self, norm=None, kwargs=None): kwargs = kwargs or {} return _NORM_FUNCTION_LOOKUP[norm](self._data, **kwargs) - def proj(self): + def proj(self) -> Qobj: """Form the projector from a given ket or bra vector. Parameters @@ -711,7 +687,7 @@ def proj(self): isherm=True, copy=False) - def tr(self): + def tr(self) -> numbers.Number: """Trace of a quantum object. Returns @@ -727,7 +703,7 @@ def tr(self): and hasattr(out, "real") ) else out - def purity(self): + def purity(self) -> numbers.Number: """Calculate purity of a quantum object. Returns @@ -744,7 +720,11 @@ def purity(self): return _data.norm.l2(self._data)**2 return _data.trace(_data.matmul(self._data, self._data)).real - def full(self, order='C', squeeze=False): + def full( + self, + order: Literal['C', 'F'] = 'C', + squeeze: bool = False + ) -> np.ndarray: """Dense array from quantum object. Parameters @@ -762,7 +742,7 @@ def full(self, order='C', squeeze=False): out = np.asarray(self.data.to_array(), order=order) return out.squeeze() if squeeze else out - def data_as(self, format=None, copy=True): + def data_as(self, format: str = None, copy: bool = True) -> Any: """Matrix from quantum object. Parameters @@ -782,7 +762,7 @@ def data_as(self, format=None, copy=True): """ return _data.extract(self._data, format, copy) - def diag(self): + def diag(self) -> np.ndarray: """Diagonal elements of quantum object. Returns @@ -798,7 +778,7 @@ def diag(self): else: return np.real(out) - def expm(self, dtype=_data.Dense): + def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """Matrix exponential of quantum operator. Input operator must be square. @@ -827,7 +807,7 @@ def expm(self, dtype=_data.Dense): isherm=self._isherm, copy=False) - def logm(self): + def logm(self) -> Qobj: """Matrix logarithm of quantum operator. Input operator must be square. @@ -849,7 +829,7 @@ def logm(self): isherm=self._isherm, copy=False) - def check_herm(self): + def check_herm(self) -> bool: """Check if the quantum object is hermitian. Returns @@ -860,7 +840,12 @@ def check_herm(self): self._isherm = None return self.isherm - def sqrtm(self, sparse=False, tol=0, maxiter=100000): + def sqrtm( + self, + sparse: bool = False, + tol: float = 0, + maxiter: int = 100000 + ) -> Qobj: """ Sqrt of a quantum operator. Operator must be square. @@ -910,7 +895,7 @@ def sqrtm(self, sparse=False, tol=0, maxiter=100000): dims=self._dims, copy=False) - def cosm(self): + def cosm(self) -> Qobj: """Cosine of a quantum operator. Operator must be square. @@ -934,7 +919,7 @@ def cosm(self): raise TypeError('invalid operand for matrix cosine') return 0.5 * ((1j * self).expm() + (-1j * self).expm()) - def sinm(self): + def sinm(self) -> Qobj: """Sine of a quantum operator. Operator must be square. @@ -957,7 +942,7 @@ def sinm(self): raise TypeError('invalid operand for matrix sine') return -0.5j * ((1j * self).expm() - (-1j * self).expm()) - def inv(self, sparse=False): + def inv(self, sparse: bool = False) -> Qobj: """Matrix inverse of a quantum operator Operator must be square. @@ -983,7 +968,12 @@ def inv(self, sparse=False): dims=[self._dims[1], self._dims[0]], copy=False) - def unit(self, inplace=False, norm=None, kwargs=None): + def unit( + self, + inplace: bool = False, + norm: Litteral["l2", "max", "fro", "tr", "one"] = None, + kwargs: dict[str, Any] = None + ) -> Qobj: """ Operator or state normalized to unity. Uses norm from Qobj.norm(). @@ -1014,7 +1004,7 @@ def unit(self, inplace=False, norm=None, kwargs=None): out = self / norm return out - def ptrace(self, sel, dtype=None): + def ptrace(self, sel: int | list[int], dtype: LayerType = None) -> Qobj: """ Take the partial trace of the quantum object leaving the selected subspaces. In other words, trace out all subspaces which are _not_ @@ -1085,7 +1075,7 @@ def ptrace(self, sel, dtype=None): return operator_to_vector(out).dag() return out - def contract(self, inplace=False): + def contract(self, inplace: bool = False) -> Qobj: """ Contract subspaces of the tensor structure which are 1D. Not defined on superoperators. If all dimensions are scalar, a Qobj of dimension @@ -1136,7 +1126,7 @@ def contract(self, inplace=False): return self return Qobj(self.data.copy(), dims=dims, copy=False) - def permute(self, order): + def permute(self, order: list) -> Qobj: """ Permute the tensor structure of a quantum object. For example, @@ -1224,7 +1214,7 @@ def permute(self, order): superrep=self.superrep, copy=False) - def tidyup(self, atol=None): + def tidyup(self, atol: float = None) -> Qobj: """ Removes small elements from the quantum object. @@ -1243,7 +1233,11 @@ def tidyup(self, atol=None): self.data = _data.tidyup(self.data, atol) return self - def transform(self, inpt, inverse=False): + def transform( + self, + inpt: list[Qobj] | ArrayLike, + inverse: bool = False + ) -> Qobj: """Basis transform defined by input array. Input array can be a ``matrix`` defining the transformation, @@ -1300,7 +1294,7 @@ def transform(self, inpt, inverse=False): superrep=self.superrep, copy=False) - def trunc_neg(self, method="clip"): + def trunc_neg(self, method: Literal["clip", "sgs"] = "clip") -> Qobj: """Truncates negative eigenvalues and renormalizes. Returns a new Qobj by removing the negative eigenvalues @@ -1357,7 +1351,7 @@ def trunc_neg(self, method="clip"): out_data = _data.mul(out_data, 1/_data.norm.trace(out_data)) return Qobj(out_data, dims=self._dims, isherm=True, copy=False) - def matrix_element(self, bra, ket): + def matrix_element(self, bra: Qobj, ket: Qobj) -> Qobj: """Calculates a matrix element. Gives the matrix element for the quantum object sandwiched between a @@ -1392,7 +1386,7 @@ def matrix_element(self, bra, ket): right = right.adjoint() return _data.inner_op(left, op, right, bra.isket) - def overlap(self, other): + def overlap(self, other: Qobj) -> numbers.Number: """ Overlap between two state vectors or two operators. @@ -1445,8 +1439,15 @@ def overlap(self, other): out = np.conj(out) return out - def eigenstates(self, sparse=False, sort='low', eigvals=0, - tol=0, maxiter=100000, phase_fix=None): + def eigenstates( + self, + sparse: bool = False, + sort: Literal["low", "high"] = 'low', + eigvals: int = 0, + tol: float = 0, + maxiter: int = 100000, + phase_fix: int = None + ) -> tuple[np.ndarray, list[Qobj]]: """Eigenstates and eigenenergies. Eigenstates and eigenenergies are defined for operators and @@ -1518,8 +1519,14 @@ def eigenstates(self, sparse=False, sort='low', eigvals=0, for ket in ekets]) return evals, ekets / norms * phase - def eigenenergies(self, sparse=False, sort='low', - eigvals=0, tol=0, maxiter=100000): + def eigenenergies( + self, + sparse: bool = False, + sort: Literal["low", "high"] = 'low', + eigvals: int = 0, + tol: float = 0, + maxiter: int = 100000, + ) -> np.ndarray: """Eigenenergies of a quantum object. Eigenenergies (eigenvalues) are defined for operators or superoperators @@ -1566,7 +1573,13 @@ def eigenenergies(self, sparse=False, sort='low', vecs=False, isherm=self._isherm, sort=sort, eigvals=eigvals) - def groundstate(self, sparse=False, tol=0, maxiter=100000, safe=True): + def groundstate( + self, + sparse: bool = False, + tol: float = 0, + maxiter: int = 100000, + safe: bool = True + ) -> tuple[float, Qobj]: """Ground state Eigenvalue and Eigenvector. Defined for quantum operators or superoperators only. @@ -1607,7 +1620,7 @@ def groundstate(self, sparse=False, tol=0, maxiter=100000, safe=True): warnings.warn("Ground state may be degenerate.", UserWarning) return evals[0], evecs[0] - def dnorm(self, B=None): + def dnorm(self, B: Qobj = None) -> float: """Calculates the diamond norm, or the diamond distance to another operator. @@ -1627,7 +1640,7 @@ def dnorm(self, B=None): return mts.dnorm(self, B) @property - def ishp(self): + def ishp(self) -> bool: # FIXME: this needs to be cached in the same ways as isherm. if self.type in ["super", "oper"]: try: @@ -1639,7 +1652,7 @@ def ishp(self): return False @property - def iscp(self): + def iscp(self) -> bool: # FIXME: this needs to be cached in the same ways as isherm. if self.type not in ["super", "oper"]: return False @@ -1654,7 +1667,7 @@ def iscp(self): return J.isherm and np.all(J.eigenenergies() >= -settings.core['atol']) @property - def istp(self): + def istp(self) -> bool: if self.type not in ['super', 'oper']: return False # Normalize to a super of type choi or chi. @@ -1682,7 +1695,7 @@ def istp(self): atol=settings.core['atol']) @property - def iscptp(self): + def iscptp(self) -> bool: if not (self.issuper or self.isoper): return False reps = ('choi', 'chi') @@ -1690,14 +1703,14 @@ def iscptp(self): return q_oper.iscp and q_oper.istp @property - def isherm(self): + def isherm(self) -> bool: if self._isherm is not None: return self._isherm self._isherm = _data.isherm(self._data) return self._isherm @isherm.setter - def isherm(self, isherm): + def isherm(self, isherm: bool): self._isherm = isherm def _calculate_isunitary(self): @@ -1712,49 +1725,49 @@ def _calculate_isunitary(self): tol=settings.core['atol']) @property - def isunitary(self): + def isunitary(self) -> bool: if self._isunitary is not None: return self._isunitary self._isunitary = self._calculate_isunitary() return self._isunitary @property - def shape(self): + def shape(self) -> tuple[int, int]: """Return the shape of the Qobj data.""" return self._data.shape @property - def isoper(self): + def isoper(self) -> bool: """Indicates if the Qobj represents an operator.""" return self._dims.type in ['oper', 'scalar'] @property - def isbra(self): + def isbra(self) -> bool: """Indicates if the Qobj represents a bra state.""" return self._dims.type in ['bra', 'scalar'] @property - def isket(self): + def isket(self) -> bool: """Indicates if the Qobj represents a ket state.""" return self._dims.type in ['ket', 'scalar'] @property - def issuper(self): + def issuper(self) -> bool: """Indicates if the Qobj represents a superoperator.""" return self._dims.type == 'super' @property - def isoperket(self): + def isoperket(self) -> bool: """Indicates if the Qobj represents a operator-ket state.""" return self._dims.type == 'operator-ket' @property - def isoperbra(self): + def isoperbra(self) -> bool: """Indicates if the Qobj represents a operator-bra state.""" return self._dims.type == 'operator-bra' -def ptrace(Q, sel): +def ptrace(Q: Qobj, sel: int | list[int]) -> Qobj: """ Partial trace of the Qobj with selected components remaining. From 295131854f6b873dbc589743840370d742cb9477 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 15 Feb 2024 12:16:36 -0500 Subject: [PATCH 019/305] Fix import issues --- qutip/core/coefficient.py | 12 +----------- qutip/core/cy/qobjevo.pyi | 18 +++++------------- qutip/core/cy/qobjevo.pyx | 30 +++++++++++------------------- qutip/core/qobj.py | 7 ++++--- qutip/solver/mesolve.py | 2 +- qutip/solver/sesolve.py | 2 +- qutip/typing.py | 32 ++++++++++++++++++++++++++++---- 7 files changed, 51 insertions(+), 52 deletions(-) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index 04ab77011e..adf2dfcafe 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -27,6 +27,7 @@ Coefficient, InterCoefficient, FunctionCoefficient, StrFunctionCoefficient, ConjCoefficient, NormCoefficient, ConstantCoefficient ) +from qutip.typing import CoefficientLike __all__ = ["coefficient", "CompilationOptions", "Coefficient", @@ -41,17 +42,6 @@ def _return(base, **kwargs): return base -CoefficientLike = Union[ - Coefficient, - str, - Callable[[float, ...], complex], - np.ndarray, - scipy.interpolate.PPoly, - scipy.interpolate.BSpline, - Any, -] - - # The `coefficient` function is dispatcher for the type of the `base` to the # function that created the `Coefficient` object. `coefficient_builders` stores # the map `type -> function(base, **kw)`. Optional module can add their diff --git a/qutip/core/cy/qobjevo.pyi b/qutip/core/cy/qobjevo.pyi index 5cd6e56b62..c55ff848d3 100644 --- a/qutip/core/cy/qobjevo.pyi +++ b/qutip/core/cy/qobjevo.pyi @@ -1,20 +1,12 @@ -from qutip.typing import LayerType +from qutip.typing import LayerType, ElementType, QobjEvoLike from qutip.core.qobj import Qobj from qutip.core.data import Data -from qutip.core.coefficient import Coefficient, CoefficientLike +from qutip.core.coefficient import Coefficient from numbers import Number -from typing import Any, overload, Callable, Dict, Tuple, List, Union, Sequence -from typing_extensions import Protocol +from numpy.typing import ArrayLike +from typing import Any, overload, Callable, Dict, Tuple, List, Union -class QEvoFunction(Protocol): - def __call__(self, t: Number, **kwargs) -> Qobj: - ... - - -Element = Union[QEvoFunction, Qobj, Sequence[Qobj, CoefficientLike]] -QobjEvoLike = Union[Qobj, QobjEvo, Element, Sequence[Element]] - class QobjEvo: dims: list isbra: bool @@ -56,7 +48,7 @@ class QobjEvo: def expect_data(self, t: Number, state: Data) -> Number: ... def matmul(self, t: Number, state: Qobj) -> Qobj: ... def matmul_data(self, t: Number, state: Data, out: Data = None) -> Data: ... - def to_list(self) -> List[Element]: ... + def to_list(self) -> List[ElementType]: ... def __add__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... def __iadd__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... def __radd__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index 6766c2dec1..0acfa43f51 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -5,15 +5,15 @@ import numpy as np import numbers import itertools from functools import partial -from typing import Union, Callable, Tuple, List, Dict -from typing_extensions import Protocol +from typing import Union import qutip from .. import Qobj from .. import data as _data from ..dimensions import Dimensions -from ..coefficient import coefficient, CompilationOptions, CoefficientLike +from ..coefficient import coefficient, CompilationOptions from ._element import * from qutip.settings import settings +from qutip.typing import QobjEvoLike from qutip.core.cy._element cimport _BaseElement from qutip.core.data cimport Dense, Data, dense @@ -189,15 +189,15 @@ cdef class QobjEvo: """ def __init__( QobjEvo self, - Q_object: QobjEvoLike, - args: Dict[str, Any] = None, + object Q_object, + dict args = None, *, - copy: bool = True, - compress: bool = True, - function_style: str = None, - tlist: ArrayLike = None, - order: int = 3, - boundary_conditions: Union[tuple, str] = None, + bint copy = True, + bint compress = True, + str function_style = None, + tlist = None, + int order = 3, + object boundary_conditions = None, ): if isinstance(Q_object, QobjEvo): self._dims = Q_object._dims @@ -1118,11 +1118,3 @@ class _Feedback: Raise an error when the dims of the e_ops / state don't match. Tell the dims to the feedback for reconstructing the Qobj. """ - - -class QEvoFunction(Protocol): - def __call__(self, t: Number, **kwargs) -> Qobj: - ... - -Element = Union[QEvoFunction, Qobj, Sequence[Qobj, CoefficientLike]] -QobjEvoLike = Union[Qobj, QobjEvo, Element, List[Element]] diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 2f470d5af5..f98a3a2c87 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -1,8 +1,7 @@ """The Quantum Object (Qobj) class, for representing quantum states and operators, and related functions. """ - -__all__ = ['Qobj', 'ptrace',] +from __future__ import annotations import functools import numbers @@ -20,6 +19,8 @@ enumerate_flat, collapse_dims_super, flatten, unflatten, Dimensions ) +__all__ = ['Qobj', 'ptrace',] + _NORM_FUNCTION_LOOKUP = { 'tr': _data.norm.trace, @@ -311,7 +312,7 @@ def data(self, data: _data.Data): f"{dims.shape} vs {self._data.shape}") self._data = data - def to(self, LayerType) -> Qobj: + def to(self, data_type: LayerType) -> Qobj: """ Convert the underlying data store of this `Qobj` into a different storage representation. diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index 6b1f1b5d38..dab1084e82 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -10,7 +10,7 @@ from typing import Any, Callable from time import time from .. import (Qobj, QobjEvo, isket, liouvillian, ket2dm, lindblad_dissipator) -from ..core.cy.qobjevo import QobjEvoLike +from ..typing import QobjEvoLike from ..core import stack_columns, unstack_columns from ..core import data as _data from .solver_base import Solver, _solver_deprecation diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 5b48394bc1..f4a39afde7 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -10,7 +10,7 @@ from typing import Any, Callable from .. import Qobj, QobjEvo from ..core import data as _data -from ..core.cy.qobjevo import QobjEvoLike +from ..typing import QobjEvoLike from .solver_base import Solver, _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback from . import Result diff --git a/qutip/typing.py b/qutip/typing.py index fe2715520b..2632a1abf1 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -1,9 +1,33 @@ -import typing +from typing import Sequence, Union, Any, Callable -from .core.cy.qobjEvo import QobjEvoLike, Element -from .core.coeffients import CoefficientLike +# from .core.cy.qobjEvo import QobjEvoLike, Element +# from .core.coeffients import CoefficientLike +from numbers import Number +from typing_extensions import Protocol +import numpy as np +import scipy.interpolate -__all__ = ["QobjEvoLike"] +__all__ = ["QobjEvoLike", "CoefficientLike", "LayerType"] + + +class QEvoFunction(Protocol): + def __call__(self, t: Number, **kwargs) -> "Qobj": + ... + + +CoefficientLike = Union[ + "Coefficient", + str, + Callable[[float, ...], complex], + np.ndarray, + scipy.interpolate.PPoly, + scipy.interpolate.BSpline, + Any, +] + +ElementType = Union[QEvoFunction, "Qobj", tuple["Qobj", CoefficientLike]] + +QobjEvoLike = Union["Qobj", "QobjEvo", ElementType, Sequence[ElementType]] LayerType = Union[str, type] From b26f9cbfa5aeca56d4d246633a70a12ca8666534 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 15 Feb 2024 12:31:12 -0500 Subject: [PATCH 020/305] Add typing extension to requirements --- requirements.txt | 1 + setup.cfg | 2 ++ 2 files changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 9fc7beade5..2776b33e20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ cython>=0.29.20 numpy>=1.22 scipy>=1.8 packaging +typing_extensions diff --git a/setup.cfg b/setup.cfg index ecb3cc0ee4..4211b7d96f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,12 +34,14 @@ install_requires = numpy>=1.22 scipy>=1.8,<1.12 packaging + typing_extensions setup_requires = numpy>=1.19 scipy>=1.8 cython>=0.29.20; python_version>='3.10' cython>=0.29.20,<3.0.3; python_version<='3.9' packaging + typing_extensions [options.packages.find] include = qutip* From aec2e9fcf61dfeb0950a84a3d8b1469af7b2ccb0 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 15 Feb 2024 14:21:47 -0500 Subject: [PATCH 021/305] Some cleaning --- qutip/core/coefficient.py | 1 - qutip/core/cy/qobjevo.pyi | 42 +++++++++++++++++++-------------------- qutip/core/cy/qobjevo.pyx | 18 ++++------------- qutip/core/qobj.py | 4 ++-- qutip/typing.py | 3 +-- requirements.txt | 1 - setup.cfg | 2 -- 7 files changed, 28 insertions(+), 43 deletions(-) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index adf2dfcafe..971be6dd8b 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -11,7 +11,6 @@ import importlib import warnings import numbers -from typing import Union, Callable, Any from collections import defaultdict from setuptools import setup, Extension try: diff --git a/qutip/core/cy/qobjevo.pyi b/qutip/core/cy/qobjevo.pyi index c55ff848d3..2730b7f98e 100644 --- a/qutip/core/cy/qobjevo.pyi +++ b/qutip/core/cy/qobjevo.pyi @@ -4,7 +4,7 @@ from qutip.core.data import Data from qutip.core.coefficient import Coefficient from numbers import Number from numpy.typing import ArrayLike -from typing import Any, overload, Callable, Dict, Tuple, List, Union +from typing import Any, overload, Callable class QobjEvo: @@ -17,23 +17,23 @@ class QobjEvo: isoperket: bool issuper: bool num_elements: int - shape: Tuple[int, int] + shape: tuple[int, int] superrep: str type: str def __init__( self, Q_object: QobjEvoLike, - args: Dict[str, Any] = None, + args: dict[str, Any] = None, *, copy: bool = True, compress: bool = True, function_style: str = None, tlist: ArrayLike = None, order: int = 3, - boundary_conditions: Union[tuple, str] = None, + boundary_conditions: tuple | str = None, ) -> None: ... @overload - def arguments(self, new_args: Dict[str, Any]) -> None: ... + def arguments(self, new_args: dict[str, Any]) -> None: ... @overload def arguments(self, **new_args) -> None: ... def compress(self) -> QobjEvo: ... @@ -48,22 +48,22 @@ class QobjEvo: def expect_data(self, t: Number, state: Data) -> Number: ... def matmul(self, t: Number, state: Qobj) -> Qobj: ... def matmul_data(self, t: Number, state: Data, out: Data = None) -> Data: ... - def to_list(self) -> List[ElementType]: ... - def __add__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... - def __iadd__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... - def __radd__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... - def __sub__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... - def __isub__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... - def __rsub__(self, other: Union[QobjEvo, Qobj, Number]) -> QobjEvo: ... - def __and__(self, other: Union[Number, Qobj]) -> QobjEvo: ... - def __rand__(self, other: Union[Number, Qobj]) -> QobjEvo: ... - def __call__(self, t: Number, **new_args) -> Qobj: ... - def __matmul__(self, other: Union[QobjEvo, Qobj]) -> QobjEvo: ... - def __imatmul__(self, other: Union[QobjEvo, Qobj]) -> QobjEvo: ... - def __rmatmul__(self, other: Union[QobjEvo, Qobj]) -> QobjEvo: ... - def __mul__(self, other: Union[Number, Coefficient]) -> QobjEvo: ... - def __imul__(self, other: Union[Number, Coefficient]) -> QobjEvo: ... - def __rmul__(self, other: Union[Number, Coefficient]) -> QobjEvo: ... + def to_list(self) -> list[ElementType]: ... + def __add__(self, other: QobjEvo | Qobj | Number) -> QobjEvo: ... + def __iadd__(self, other: QobjEvo | Qobj | Number) -> QobjEvo: ... + def __radd__(self, other: QobjEvo | Qobj | Number) -> QobjEvo: ... + def __sub__(self, other: QobjEvo | Qobj | Number) -> QobjEvo: ... + def __isub__(self, other: QobjEvo | Qobj | Number) -> QobjEvo: ... + def __rsub__(self, other: QobjEvo | Qobj | Number) -> QobjEvo: ... + def __and__(self, other: Qobj | QobjEvo) -> QobjEvo: ... + def __rand__(self, other: Qobj | QobjEvo) -> QobjEvo: ... + def __call__(self, t: float, **new_args) -> Qobj: ... + def __matmul__(self, other: Qobj | QobjEvo) -> QobjEvo: ... + def __imatmul__(self, other: Qobj | QobjEvo) -> QobjEvo: ... + def __rmatmul__(self, other: Qobj | QobjEvo) -> QobjEvo: ... + def __mul__(self, other: Number | Coefficient) -> QobjEvo: ... + def __imul__(self, other: Number | Coefficient) -> QobjEvo: ... + def __rmul__(self, other: Number | Coefficient) -> QobjEvo: ... def __truediv__(self, other : Number) -> QobjEvo: ... def __idiv__(self, other : Number) -> QobjEvo: ... def __neg__(self) -> QobjEvo: ... diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index 0acfa43f51..c2b4d5d3ae 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -5,7 +5,7 @@ import numpy as np import numbers import itertools from functools import partial -from typing import Union + import qutip from .. import Qobj from .. import data as _data @@ -13,7 +13,6 @@ from ..dimensions import Dimensions from ..coefficient import coefficient, CompilationOptions from ._element import * from qutip.settings import settings -from qutip.typing import QobjEvoLike from qutip.core.cy._element cimport _BaseElement from qutip.core.data cimport Dense, Data, dense @@ -187,18 +186,9 @@ cdef class QobjEvo: qevo = H0 + H1 * coeff """ - def __init__( - QobjEvo self, - object Q_object, - dict args = None, - *, - bint copy = True, - bint compress = True, - str function_style = None, - tlist = None, - int order = 3, - object boundary_conditions = None, - ): + def __init__(QobjEvo self, Q_object, args=None, *, copy=True, compress=True, + function_style=None, + tlist=None, order=3, boundary_conditions=None): if isinstance(Q_object, QobjEvo): self._dims = Q_object._dims self.shape = Q_object.shape diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index f98a3a2c87..d36d86b28c 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -19,7 +19,7 @@ enumerate_flat, collapse_dims_super, flatten, unflatten, Dimensions ) -__all__ = ['Qobj', 'ptrace',] +__all__ = ['Qobj', 'ptrace'] _NORM_FUNCTION_LOOKUP = { @@ -1621,7 +1621,7 @@ def groundstate( warnings.warn("Ground state may be degenerate.", UserWarning) return evals[0], evecs[0] - def dnorm(self, B: Qobj = None) -> float: + def dnorm(self, B: Qobj = None) -> numbers.Number: """Calculates the diamond norm, or the diamond distance to another operator. diff --git a/qutip/typing.py b/qutip/typing.py index 2632a1abf1..dec39c7281 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -1,9 +1,8 @@ -from typing import Sequence, Union, Any, Callable +from typing import Sequence, Union, Any, Callable, Protocol # from .core.cy.qobjEvo import QobjEvoLike, Element # from .core.coeffients import CoefficientLike from numbers import Number -from typing_extensions import Protocol import numpy as np import scipy.interpolate diff --git a/requirements.txt b/requirements.txt index 2776b33e20..9fc7beade5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,3 @@ cython>=0.29.20 numpy>=1.22 scipy>=1.8 packaging -typing_extensions diff --git a/setup.cfg b/setup.cfg index 4211b7d96f..ecb3cc0ee4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,14 +34,12 @@ install_requires = numpy>=1.22 scipy>=1.8,<1.12 packaging - typing_extensions setup_requires = numpy>=1.19 scipy>=1.8 cython>=0.29.20; python_version>='3.10' cython>=0.29.20,<3.0.3; python_version<='3.9' packaging - typing_extensions [options.packages.find] include = qutip* From 3b337ba7e03b16e97f0771a0691434385b21a1e2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 15 Feb 2024 14:50:13 -0500 Subject: [PATCH 022/305] Protocul use in CoefficientLike definition --- qutip/typing.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/qutip/typing.py b/qutip/typing.py index dec39c7281..f5d86757c2 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -2,7 +2,7 @@ # from .core.cy.qobjEvo import QobjEvoLike, Element # from .core.coeffients import CoefficientLike -from numbers import Number +from numbers import Number, Real import numpy as np import scipy.interpolate @@ -10,22 +10,27 @@ __all__ = ["QobjEvoLike", "CoefficientLike", "LayerType"] -class QEvoFunction(Protocol): - def __call__(self, t: Number, **kwargs) -> "Qobj": +class QEvoProtocol(Protocol): + def __call__(self, t: Real, **kwargs) -> "Qobj": + ... + + +class CoeffProtocol(Protocol): + def __call__(self, t: Real, **kwargs) -> Number: ... CoefficientLike = Union[ "Coefficient", str, - Callable[[float, ...], complex], + CoeffProtocol, np.ndarray, scipy.interpolate.PPoly, scipy.interpolate.BSpline, Any, ] -ElementType = Union[QEvoFunction, "Qobj", tuple["Qobj", CoefficientLike]] +ElementType = Union[QEvoProtocol, "Qobj", tuple["Qobj", CoefficientLike]] QobjEvoLike = Union["Qobj", "QobjEvo", ElementType, Sequence[ElementType]] From 75944b8db0c0b384e01da2d612beb2e45726f1eb Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 16 Feb 2024 17:11:55 -0500 Subject: [PATCH 023/305] Setting for isherm -> real --- qutip/core/expect.py | 12 ++++++++---- qutip/core/options.py | 3 +++ qutip/core/qobj.py | 13 ++++++------- qutip/solver/solver_base.py | 2 +- qutip/tests/core/test_expect.py | 18 ++++-------------- qutip/tests/core/test_qobj.py | 28 ++++++++++++---------------- 6 files changed, 34 insertions(+), 42 deletions(-) diff --git a/qutip/core/expect.py b/qutip/core/expect.py index 9051c53f72..5cf01b09f0 100644 --- a/qutip/core/expect.py +++ b/qutip/core/expect.py @@ -4,6 +4,7 @@ from .qobj import Qobj from . import data as _data +from ..settings import settings def expect(oper, state): @@ -71,10 +72,13 @@ def _single_qobj_expect(oper, state): # This ensures that expect can return something that is not a number such # as a `tensorflow.Tensor` in qutip-tensorflow. - return out.real if (oper.isherm - and (state.isket or state.isherm) - and hasattr(out, "real") - ) else out + if ( + settings.core["auto_real_casting"] + and oper.isherm + and (state.isket or state.isherm) + ): + out = out.real + return out def variance(oper, state): diff --git a/qutip/core/options.py b/qutip/core/options.py index 4748dd3612..e25134b345 100644 --- a/qutip/core/options.py +++ b/qutip/core/options.py @@ -124,6 +124,9 @@ class CoreOptions(QutipOptions): "function_coefficient_style": "auto", # Default Qobj dtype for Qobj create function "default_dtype": None, + # Expect, trace, etc. will return real for hermitian matrices. + # Hermiticity checks can be slow, stop jitting, etc. + "auto_real_casting": True, } _settings_name = "core" diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 2b4dcda24f..198758b92d 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -727,9 +727,9 @@ def tr(self): out = _data.trace(self._data) # This ensures that trace can return something that is not a number such # as a `tensorflow.Tensor` in qutip-tensorflow. - return out.real if (self.isherm - and hasattr(out, "real") - ) else out + if settings.core["auto_real_casting"] and self.isherm: + out = out.real + return out def purity(self): """Calculate purity of a quantum object. @@ -797,10 +797,9 @@ def diag(self): """ # TODO: add a `diagonal` method to the data layer? out = _data.to(_data.CSR, self.data).as_scipy().diagonal() - if np.any(np.imag(out) > settings.core['atol']) or not self.isherm: - return out - else: - return np.real(out) + if settings.core["auto_real_casting"] and self.isherm: + out = np.real(out) + return out def expm(self, dtype=_data.Dense): """Matrix exponential of quantum operator. diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 28a4c8ba60..045d69aa01 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -85,7 +85,7 @@ def _prepare_state(self, state): self._state_metadata = { 'dims': state.dims, - 'isherm': state.isherm and not (self.rhs.dims == state.dims) + 'isherm': state._isherm if self.rhs.dims != state.dims else False } if self.rhs.dims[1] == state.dims: return stack_columns(state.data) diff --git a/qutip/tests/core/test_expect.py b/qutip/tests/core/test_expect.py index 906880ceca..f461a36f59 100644 --- a/qutip/tests/core/test_expect.py +++ b/qutip/tests/core/test_expect.py @@ -162,18 +162,8 @@ def test_compatibility_with_solver(solve): np.testing.assert_allclose(np.array(direct_), indirect_, atol=1e-12) -def test_no_real_attribute(monkeypatch): - """This tests ensures that expect still works even if the output of a - specialisation does not have the ``real`` attribute. This is the case for - the tensorflow and cupy data layers.""" - - def mocker_expect_return(oper, state): - """ - We simply return None which does not have the `real` attribute. - """ - return "object without .real" - - monkeypatch.setattr(_data, "expect", mocker_expect_return) - +def test_no_real_casting(monkeypatch): sz = qutip.sigmaz() # the choice of the matrix does not matter - assert "object without .real" == qutip.expect(sz, sz) + assert isinstance(qutip.expect(sz, sz), float) + with qutip.CoreOptions(auto_real_casting=False): + assert isinstance(qutip.expect(sz, sz), complex) diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py index 7aa7dc0b3b..45f54fc3cc 100644 --- a/qutip/tests/core/test_qobj.py +++ b/qutip/tests/core/test_qobj.py @@ -552,6 +552,12 @@ def test_QobjDiagonals(): assert np.all(b == np.diag(data)) +def test_diag_type(): + assert qutip.sigmaz().diag().dtype == np.float64 + assert (1j * qutip.sigmaz()).diag().dtype == np.complex128 + with qutip.CoreOptions(auto_real_casting=False): + assert qutip.sigmaz().diag().dtype == np.complex128 + def test_QobjEigenEnergies(): "qutip.Qobj eigenenergies" data = np.eye(5) @@ -1123,21 +1129,11 @@ def test_trace(): assert sz.tr() == 0 -def test_no_real_attribute(monkeypatch): - """This tests ensures that trace still works even if the output of a - specialisation does not have the ``real`` attribute. This is the case for - the tensorflow and cupy data layers.""" - - def mocker_trace_return(oper): - """ - We simply return a string which does not have the `real` attribute. - """ - return "object without .real" - - monkeypatch.setattr(_data, "trace", mocker_trace_return) - - sz = qutip.sigmaz() # the choice of the matrix does not matter - assert "object without .real" == sz.tr() +def test_no_real_casting(): + sz = qutip.sigmaz() + assert isinstance(sz.tr(), float) + with qutip.CoreOptions(auto_real_casting=False): + assert isinstance(sz.tr(), complex) @pytest.mark.parametrize('inplace', [True, False], ids=['inplace', 'new']) @@ -1267,4 +1263,4 @@ def test_data_as(): @pytest.mark.parametrize('dtype', ["CSR", "Dense"]) def test_qobj_dtype(dtype): obj = qutip.qeye(2, dtype=dtype) - assert obj.dtype == qutip.data.to.parse(dtype) \ No newline at end of file + assert obj.dtype == qutip.data.to.parse(dtype) From 05cc4d730871290ab1f16cba78ab69841d189493 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 19 Feb 2024 12:22:34 -0500 Subject: [PATCH 024/305] Add towncrier --- doc/changes/2329.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2329.misc diff --git a/doc/changes/2329.misc b/doc/changes/2329.misc new file mode 100644 index 0000000000..73e562b4a3 --- /dev/null +++ b/doc/changes/2329.misc @@ -0,0 +1 @@ +Add auto_real_casting options. From a3a9bb471a1163d8a8d5c57aae5780b9c7320252 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 14 Mar 2024 11:40:12 -0400 Subject: [PATCH 025/305] Fix reshape_dense integer overflow --- qutip/core/data/reshape.pyx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/qutip/core/data/reshape.pyx b/qutip/core/data/reshape.pyx index 4b9683ac60..daec008272 100644 --- a/qutip/core/data/reshape.pyx +++ b/qutip/core/data/reshape.pyx @@ -2,7 +2,7 @@ #cython: boundscheck=False, wraparound=False, initializedcheck=False, cdivision=True from libc.string cimport memcpy, memset - +from scipy.linalg cimport cython_blas as blas cimport cython import warnings @@ -52,7 +52,7 @@ cpdef CSR reshape_csr(CSR matrix, idxint n_rows_out, idxint n_cols_out): return out -cdef inline idxint _reshape_dense_reindex(idxint idx, idxint size): +cdef inline size_t _reshape_dense_reindex(size_t idx, size_t size): return (idx // size) + (idx % size) @@ -66,8 +66,9 @@ cpdef Dense reshape_dense(Dense matrix, idxint n_rows_out, idxint n_cols_out): out = dense.zeros(n_rows_out, n_cols_out) cdef size_t idx_in=0, idx_out=0 cdef size_t size = n_rows_out * n_cols_out + cdef size_t tmp = ( matrix.shape[1]) * ( n_rows_out) # TODO: improve the algorithm here. - cdef size_t stride = _reshape_dense_reindex(matrix.shape[1]*n_rows_out, size) + cdef size_t stride = _reshape_dense_reindex(tmp, size) for idx_in in range(size): out.data[idx_out] = matrix.data[idx_in] idx_out = _reshape_dense_reindex(idx_out + stride, size) @@ -99,7 +100,16 @@ cpdef Dense column_stack_dense(Dense matrix, bint inplace=False): return out if inplace: warnings.warn("cannot stack columns inplace for C-ordered matrix") - return reshape_dense(matrix.transpose(), matrix.shape[0]*matrix.shape[1], 1) + out = dense.zeros(matrix.shape[0] * matrix.shape[1], 1) + cdef idxint col + cdef int ONE=1 + for col in range(matrix.shape[1]): + blas.zcopy( + &matrix.shape[0], + &matrix.data[col], &matrix.shape[1], + &out.data[col * matrix.shape[0]], &ONE + ) + return out cpdef Dia column_stack_dia(Dia matrix): From ebd0bad1e7a5c02f95a67f928ab8ffce36a77e28 Mon Sep 17 00:00:00 2001 From: rochisha0 Date: Sat, 16 Mar 2024 14:47:15 +0530 Subject: [PATCH 026/305] add dtype to printed output of qobj --- qutip/core/qobj.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 0b52ad6480..942fbff019 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -533,6 +533,7 @@ def _str_header(self): "Quantum object: dims=" + str(self.dims), "shape=" + str(self._data.shape), "type=" + repr(self.type), + "dtype=" + str(type(self.type)), ]) if self.type in ('oper', 'super'): out += ", isherm=" + str(self.isherm) From e45387b579c94fb147050285b309af9dd1517081 Mon Sep 17 00:00:00 2001 From: rochisha0 Date: Sat, 16 Mar 2024 16:18:32 +0530 Subject: [PATCH 027/305] add towncrier entry --- doc/changes/2352.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2352.feature diff --git a/doc/changes/2352.feature b/doc/changes/2352.feature new file mode 100644 index 0000000000..96b9a87c15 --- /dev/null +++ b/doc/changes/2352.feature @@ -0,0 +1 @@ +Add dtype to printed ouput of qobj \ No newline at end of file From 5fd6d93299507432db7ddb984764cab66b89aadb Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 19 Mar 2024 17:00:15 +0100 Subject: [PATCH 028/305] Remove pin on scipy 1.12 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index ecb3cc0ee4..7505274144 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,7 @@ include_package_data = True zip_safe = False install_requires = numpy>=1.22 - scipy>=1.8,<1.12 + scipy>=1.8 packaging setup_requires = numpy>=1.19 From d40b287daab1f8dc873e62818854b807834fda9c Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Tue, 19 Mar 2024 19:29:36 +0100 Subject: [PATCH 029/305] add news entry --- doc/changes/2354.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2354.misc diff --git a/doc/changes/2354.misc b/doc/changes/2354.misc new file mode 100644 index 0000000000..e3cfa9de56 --- /dev/null +++ b/doc/changes/2354.misc @@ -0,0 +1 @@ +Allow scipy 1.12 to be used with qutip. \ No newline at end of file From 642aa6f200f79ffc609fe0ae3afbe3bfdd823b7e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 21 Mar 2024 22:01:20 +0900 Subject: [PATCH 030/305] Fixed Qobj string representation --- qutip/core/qobj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 942fbff019..4c498bc09a 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -533,7 +533,7 @@ def _str_header(self): "Quantum object: dims=" + str(self.dims), "shape=" + str(self._data.shape), "type=" + repr(self.type), - "dtype=" + str(type(self.type)), + "dtype=" + self.dtype.__name__, ]) if self.type in ('oper', 'super'): out += ", isherm=" + str(self.isherm) From 8b833ae94b64cd65ff5eea61bce986a180d714c8 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 10:54:20 +0900 Subject: [PATCH 031/305] Added test --- qutip/tests/core/test_qobj.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py index 7aa7dc0b3b..46a7f6e8a3 100644 --- a/qutip/tests/core/test_qobj.py +++ b/qutip/tests/core/test_qobj.py @@ -1264,7 +1264,13 @@ def test_data_as(): assert "dia_matrix" in str(err.value) -@pytest.mark.parametrize('dtype', ["CSR", "Dense"]) +@pytest.mark.parametrize('dtype', ["CSR", "Dense", "Dia"]) def test_qobj_dtype(dtype): obj = qutip.qeye(2, dtype=dtype) - assert obj.dtype == qutip.data.to.parse(dtype) \ No newline at end of file + assert obj.dtype == qutip.data.to.parse(dtype) + + +@pytest.mark.parametrize('dtype', ["CSR", "Dense", "Dia"]) +def test_dtype_in_info_string(dtype): + obj = qutip.qeye(2, dtype=dtype) + assert dtype.lower() in str(obj).lower() \ No newline at end of file From beda9206113c3174c899d899e92da18d7da36265 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 10:58:14 +0900 Subject: [PATCH 032/305] normalize_output should be bool --- qutip/solver/nonmarkov/transfertensor.py | 2 +- qutip/solver/solver_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/solver/nonmarkov/transfertensor.py b/qutip/solver/nonmarkov/transfertensor.py index ce8bea512f..386bdd4ee8 100644 --- a/qutip/solver/nonmarkov/transfertensor.py +++ b/qutip/solver/nonmarkov/transfertensor.py @@ -71,7 +71,7 @@ def ttmsolve(dynmaps, state0, times, e_ops=(), num_learning=0, options=None): opt = { "store_final_state": False, "store_states": None, - "normalize_output": "ket", + "normalize_output": True, "threshold": 0.0, "num_learning": 0, } diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 67aa473379..d27c5f2786 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -38,7 +38,7 @@ class Solver: "progress_kwargs": {"chunk_size": 10}, "store_final_state": False, "store_states": None, - "normalize_output": "ket", + "normalize_output": True, "method": "adams", } _resultclass = Result From 5bb1108dd08c0a7c7fffd400f2d4d5d64ba99af1 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 11:04:45 +0900 Subject: [PATCH 033/305] Weiner -> Wiener --- qutip/solver/_feedback.py | 10 +++++----- qutip/solver/stochastic.py | 16 ++++++++-------- qutip/tests/solver/test_stochastic.py | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/qutip/solver/_feedback.py b/qutip/solver/_feedback.py index 79d82259cc..2d80d9ff16 100644 --- a/qutip/solver/_feedback.py +++ b/qutip/solver/_feedback.py @@ -133,18 +133,18 @@ def __repr__(self): return "CollapseFeedback" -def _default_weiner(t): +def _default_wiener(t): return np.zeros(1) -class _WeinerFeedback(_Feedback): - code = "WeinerFeedback" +class _WienerFeedback(_Feedback): + code = "WienerFeedback" def __init__(self, default=None): - self.default = default or _default_weiner + self.default = default or _default_wiener def check_consistency(self, dims): pass def __repr__(self): - return "WeinerFeedback" + return "WienerFeedback" diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index f39837c956..af57fe40a1 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -8,7 +8,7 @@ import numpy as np from functools import partial from .solver_base import _solver_deprecation -from ._feedback import _QobjFeedback, _DataFeedback, _WeinerFeedback +from ._feedback import _QobjFeedback, _DataFeedback, _WienerFeedback class StochasticTrajResult(Result): @@ -235,11 +235,11 @@ def _register_feedback(self, val): self.H._register_feedback({"wiener_process": val}, "stochatic solver") for c_op in self.c_ops: c_op._register_feedback( - {"WeinerFeedback": val}, "stochatic solver" + {"WienerFeedback": val}, "stochatic solver" ) for sc_op in self.sc_ops: sc_op._register_feedback( - {"WeinerFeedback": val}, "stochatic solver" + {"WienerFeedback": val}, "stochatic solver" ) @@ -713,13 +713,13 @@ def options(self, new_options): MultiTrajSolver.options.fset(self, new_options) @classmethod - def WeinerFeedback(cls, default=None): + def WienerFeedback(cls, default=None): """ - Weiner function of the trajectory argument for time dependent systems. + Wiener function of the trajectory argument for time dependent systems. When used as an args: - ``QobjEvo([op, func], args={"W": SMESolver.WeinerFeedback()})`` + ``QobjEvo([op, func], args={"W": SMESolver.WienerFeedback()})`` The ``func`` will receive a function as ``W`` that return an array of wiener processes values at ``t``. The wiener process for the i-th @@ -729,7 +729,7 @@ def WeinerFeedback(cls, default=None): .. note:: - WeinerFeedback can't be added to a running solver when updating + WienerFeedback can't be added to a running solver when updating arguments between steps: ``solver.step(..., args={})``. Parameters @@ -739,7 +739,7 @@ def WeinerFeedback(cls, default=None): When not passed, a function returning ``np.array([0])`` is used. """ - return _WeinerFeedback(default) + return _WienerFeedback(default) @classmethod def StateFeedback(cls, default=None, raw_data=False): diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 32a42265b1..b692312c38 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -334,7 +334,7 @@ def func(t, A, W): [destroy(N), func], args={ "A": SMESolver.ExpectFeedback(num(10)), - "W": SMESolver.WeinerFeedback() + "W": SMESolver.WienerFeedback() } )] psi0 = basis(N, N-3) From 32d5d2f7107f1c99113c5ce916c1f79bd6e5b9d0 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 11:07:38 +0900 Subject: [PATCH 034/305] matricies -> matrices --- qutip/simdiag.py | 6 +++--- qutip/solver/result.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/qutip/simdiag.py b/qutip/simdiag.py index 63d2a6e107..9277a38ccc 100644 --- a/qutip/simdiag.py +++ b/qutip/simdiag.py @@ -79,15 +79,15 @@ def simdiag(ops, evals: bool = True, *, A = ops[jj] shape = A.shape if shape[0] != shape[1]: - raise TypeError('Matricies must be square.') + raise TypeError('Matrices must be square.') if shape[0] != N: raise TypeError('All matrices. must be the same shape') if not A.isherm: - raise TypeError('Matricies must be Hermitian') + raise TypeError('Matrices must be Hermitian') for kk in range(jj): B = ops[kk] if (A * B - B * A).norm() / (A * B).norm() > tol: - raise TypeError('Matricies must commute.') + raise TypeError('Matrices must commute.') # TODO: rewrite using Data object eigvals, eigvecs = _data.eigs(ops[0].data, True, True) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 2f32dddbc9..3e22f7fea0 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -526,7 +526,7 @@ def __init__( self._post_init(**kw) @property - def _store_average_density_matricies(self) -> bool: + def _store_average_density_matrices(self) -> bool: return ( self.options["store_states"] or (self.options["store_states"] is None and self._raw_ops == {}) @@ -536,7 +536,7 @@ def _store_average_density_matricies(self) -> bool: def _store_final_density_matrix(self) -> bool: return ( self.options["store_final_state"] - and not self._store_average_density_matricies + and not self._store_average_density_matrices and not self.options["keep_runs_results"] ) @@ -552,7 +552,7 @@ def _add_first_traj(self, trajectory): """ self.times = trajectory.times - if trajectory.states and self._store_average_density_matricies: + if trajectory.states and self._store_average_density_matrices: self._sum_states = [ qzero_like(self._to_dm(state)) for state in trajectory.states ] @@ -668,7 +668,7 @@ def _post_init(self): store_trajectory = self.options["keep_runs_results"] if store_trajectory: self.add_processor(self._store_trajectory) - if self._store_average_density_matricies: + if self._store_average_density_matrices: self.add_processor(self._reduce_states) if self._store_final_density_matrix: self.add_processor(self._reduce_final_state) @@ -1131,7 +1131,7 @@ def _average_computer(self): def _add_first_traj(self, trajectory): super()._add_first_traj(trajectory) - if trajectory.states and self._store_average_density_matricies: + if trajectory.states and self._store_average_density_matrices: del self._sum_states self._sum_states_no_jump = [ qzero_like(self._to_dm(state)) for state in trajectory.states From 572c5b9a3efa650beb4d4193dc626ce708488f9e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 13:40:49 +0900 Subject: [PATCH 035/305] Utilize integrator.run in MultiTrajSolver --- qutip/solver/multitraj.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 3ca7930a96..59587a48c7 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -248,8 +248,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops): return self._integrate_one_traj(seed, tlist, result) def _integrate_one_traj(self, seed, tlist, result): - for t in tlist[1:]: - t, state = self._integrator.integrate(t, copy=False) + for t, state in self._integrator.run(tlist): result.add(t, self._restore_state(state, copy=False)) return seed, result From 4d1873620cbe474b13fa8dd5afdfb09941397ad0 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 13:48:57 +0900 Subject: [PATCH 036/305] Handling of arguments in multi trajectory solver --- qutip/solver/mcsolve.py | 4 ---- qutip/solver/multitraj.py | 1 + 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 3c100906d4..8f1bf9cfe2 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -373,10 +373,6 @@ def _do_collapse(self, collapse_time, state): def arguments(self, args): if args: self._integrator.arguments(args) - for c_op in self._c_ops: - c_op.arguments(args) - for n_op in self._n_ops: - n_op.arguments(args) @property def integrator_options(self): diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 59587a48c7..6df866a62b 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -278,6 +278,7 @@ def _argument(self, args): """Update the args, for the `rhs` and `c_ops` and other operators.""" if args: self.system.arguments(args) + self._integrator.arguments(args) def _get_generator(self, seed): """ From 6782282e60a138c7c4895930c37c2cd5fb25f686 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 14:05:09 +0900 Subject: [PATCH 037/305] Documented reason for _restore_state in MCSolver --- qutip/solver/mcsolve.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 8f1bf9cfe2..8f1b6abf29 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -459,6 +459,8 @@ def _restore_state(self, data, *, copy=True): """ Retore the Qobj state from its data. """ + # Duplicated from the Solver class, but removed the check for the + # normalize_output option, since MCSolver doesn't have that option. if self._state_metadata['dims'] == self.rhs._dims[1]: state = Qobj(unstack_columns(data), **self._state_metadata, copy=False) From 9d96cd7892495aab0621bce4b8aeff2e8673a662 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 14:17:47 +0900 Subject: [PATCH 038/305] Reduced code duplication in mcsolve --- qutip/solver/mcsolve.py | 22 +++++----------------- qutip/solver/multitraj.py | 11 +++++++---- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 8f1b6abf29..b7cf01957a 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -478,25 +478,12 @@ def _initialize_stats(self): }) return stats - def _initialize_run_one_traj(self, seed, state, tlist, e_ops, - no_jump=False, jump_prob_floor=0.0): - result = self._trajectory_resultclass(e_ops, self.options) - generator = self._get_generator(seed) - self._integrator.set_state(tlist[0], state, generator, - no_jump=no_jump, - jump_prob_floor=jump_prob_floor) - result.add(tlist[0], self._restore_state(state, copy=False)) - return result - - def _run_one_traj(self, seed, state, tlist, e_ops, no_jump=False, - jump_prob_floor=0.0): + def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ Run one trajectory and return the result. """ - result = self._initialize_run_one_traj(seed, state, tlist, e_ops, - no_jump=no_jump, - jump_prob_floor=jump_prob_floor) - seed, result = self._integrate_one_traj(seed, tlist, result) + seed, result = super()._run_one_traj(seed, state, tlist, e_ops, + **integrator_kwargs) result.collapse = self._integrator.collapses return seed, result @@ -538,7 +525,8 @@ def run(self, state, tlist, ntraj=1, *, start_time = time() map_func( self._run_one_traj, seeds[1:], - (state0, tlist, e_ops, False, no_jump_prob), + task_args=(state0, tlist, e_ops), + task_kwargs={'no_jump': False, 'jump_prob_floor': no_jump_prob}, reduce_func=result.add, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"] diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 6df866a62b..16ce13bec8 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -233,18 +233,21 @@ def run(self, state, tlist, ntraj=1, *, result.stats['run time'] = time() - start_time return result - def _initialize_run_one_traj(self, seed, state, tlist, e_ops): + def _initialize_run_one_traj(self, seed, state, tlist, e_ops, + **integrator_kwargs): result = self._trajectory_resultclass(e_ops, self.options) generator = self._get_generator(seed) - self._integrator.set_state(tlist[0], state, generator) + self._integrator.set_state(tlist[0], state, generator, + **integrator_kwargs) result.add(tlist[0], self._restore_state(state, copy=False)) return result - def _run_one_traj(self, seed, state, tlist, e_ops): + def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ Run one trajectory and return the result. """ - result = self._initialize_run_one_traj(seed, state, tlist, e_ops) + result = self._initialize_run_one_traj(seed, state, tlist, e_ops, + **integrator_kwargs) return self._integrate_one_traj(seed, tlist, result) def _integrate_one_traj(self, seed, tlist, result): From f395b609a542d23c2da14392a4f044467915d90e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 22 Mar 2024 14:37:19 +0900 Subject: [PATCH 039/305] Fix MCSolver.run docstring --- qutip/solver/mcsolve.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index b7cf01957a..e2d2f87342 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -489,13 +489,9 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): def run(self, state, tlist, ntraj=1, *, args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): - """ - Do the evolution of the Quantum system. - See the overridden method for further details. The modification - here is to sample the no-jump trajectory first. Then, the no-jump - probability is used as a lower-bound for random numbers in future - monte carlo runs - """ + # Overridden to sample the no-jump trajectory first. Then, the no-jump + # probability is used as a lower-bound for random numbers in future + # monte carlo runs if not self.options.get("improved_sampling", False): return super().run(state, tlist, ntraj=ntraj, args=args, e_ops=e_ops, timeout=timeout, From 099471332d3882003e0d1dd5204718796c69ce08 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Sun, 24 Mar 2024 13:04:12 +0900 Subject: [PATCH 040/305] WIP commit: weighted trajectories --- doc/apidoc/classes.rst | 6 +- qutip/solver/mcsolve.py | 29 +-- qutip/solver/multitraj.py | 9 +- qutip/solver/nm_mcsolve.py | 55 ++-- qutip/solver/result.py | 518 ++++++++++++++++++++++--------------- 5 files changed, 369 insertions(+), 248 deletions(-) diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index ecd0cd7a97..375c71542b 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -236,15 +236,13 @@ Solver Options and Results :inherited-members: :exclude-members: add_processor, add, add_end_condition +.. autoclass:: qutip.solver.result.TrajectoryResult + .. autoclass:: qutip.solver.result.McResult :members: :inherited-members: :exclude-members: add_processor, add, add_end_condition - -.. autoclass:: qutip.solver.result.NmmcResult :members: - :inherited-members: - :exclude-members: add_processor, add, add_end_condition .. _classes-piqs: diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index e2d2f87342..b1324fb034 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -4,7 +4,7 @@ from ..core import QobjEvo, spre, spost, Qobj, unstack_columns from .multitraj import MultiTrajSolver, _MTSystem from .solver_base import Solver, Integrator, _solver_deprecation -from .result import McResult, McTrajectoryResult, McResultImprovedSampling +from .result import McResult from .mesolve import mesolve, MESolver from ._feedback import _QobjFeedback, _DataFeedback, _CollapseFeedback import qutip.core.data as _data @@ -405,7 +405,7 @@ class MCSolver(MultiTrajSolver): Options for the evolution. """ name = "mcsolve" - _trajectory_resultclass = McTrajectoryResult + _resultclass = McResult _mc_integrator_class = MCIntegrator solver_options = { "progress_bar": "text", @@ -484,6 +484,9 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ seed, result = super()._run_one_traj(seed, state, tlist, e_ops, **integrator_kwargs) + jump_prob_floor = integrator_kwargs.get('jump_prob_floor', 0) + if jump_prob_floor > 0: + result.add_relative_weight(1 - jump_prob_floor) result.collapse = self._integrator.collapses return seed, result @@ -492,26 +495,23 @@ def run(self, state, tlist, ntraj=1, *, # Overridden to sample the no-jump trajectory first. Then, the no-jump # probability is used as a lower-bound for random numbers in future # monte carlo runs - if not self.options.get("improved_sampling", False): + if not self.options["improved_sampling"]: return super().run(state, tlist, ntraj=ntraj, args=args, e_ops=e_ops, timeout=timeout, target_tol=target_tol, seeds=seeds) + stats, seeds, result, map_func, map_kw, state0 = self._initialize_run( - state, - ntraj, - args=args, - e_ops=e_ops, - timeout=timeout, - target_tol=target_tol, - seeds=seeds, + state, ntraj, args=args, timeout=timeout, + target_tol=target_tol, seeds=seeds, ) + # first run the no-jump trajectory start_time = time() seed0, no_jump_result = self._run_one_traj(seeds[0], state0, tlist, e_ops, no_jump=True) _, state, _ = self._integrator.get_state(copy=False) no_jump_prob = self._integrator._prob_func(state) - result.no_jump_prob = no_jump_prob + no_jump_result.add_absolute_weight(no_jump_prob) result.add((seed0, no_jump_result)) result.stats['no jump run time'] = time() - start_time @@ -546,13 +546,6 @@ def _get_integrator(self): self._init_integrator_time = time() - _time_start return mc_integrator - @property - def _resultclass(self): - if self.options.get("improved_sampling", False): - return McResultImprovedSampling - else: - return McResult - @property def options(self): """ diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 16ce13bec8..f82338dc0d 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -1,4 +1,4 @@ -from .result import Result, MultiTrajResult +from .result import TrajectoryResult, MultiTrajResult from .parallel import _get_map from time import time from .solver_base import Solver @@ -51,7 +51,7 @@ class MultiTrajSolver(Solver): """ name = "generic multi trajectory" _resultclass = MultiTrajResult - _trajectory_resultclass = Result + _trajectory_resultclass = TrajectoryResult _avail_integrators = {} # Class of option used by the solver @@ -131,7 +131,7 @@ def step(self, t, *, args=None, copy=True): _, state = self._integrator.integrate(t, copy=False) return self._restore_state(state, copy=copy) - def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), + def _initialize_run(self, state, ntraj=1, args=None, timeout=None, target_tol=None, seeds=None): start_time = time() self._argument(args) @@ -139,7 +139,7 @@ def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), seeds = self._read_seed(seeds, ntraj) result = self._resultclass( - e_ops, self.options, solver=self.name, stats=stats + self.options, solver=self.name, stats=stats ) result.add_end_condition(ntraj, target_tol) @@ -217,7 +217,6 @@ def run(self, state, tlist, ntraj=1, *, state, ntraj, args=args, - e_ops=e_ops, timeout=timeout, target_tol=target_tol, seeds=seeds, diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 6c064bb975..26d4118265 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -1,6 +1,5 @@ __all__ = ['nm_mcsolve', 'NonMarkovianMCSolver'] -import functools import numbers import numpy as np @@ -9,7 +8,6 @@ from .multitraj import MultiTrajSolver from .mcsolve import MCSolver, MCIntegrator from .mesolve import MESolver, mesolve -from .result import NmmcResult, NmmcTrajectoryResult from .cy.nm_mcsolve import RateShiftCoefficient, SqrtRealCoefficient from ..core.coefficient import ConstantCoefficient from ..core import ( @@ -114,6 +112,9 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, ``norm_tol`` are the tolerance in time and norm respectively. An error will be raised if the collapse could not be found within ``norm_steps`` tries. + - | improved_sampling : Bool + | Whether to use the improved sampling algorithm from Abdelhafez et + al. PRA (2019) - | mc_corr_eps : float | Small number used to detect non-physical collapse caused by numerical imprecision. @@ -217,7 +218,10 @@ def initialize(self, t0, cache='clear'): # to pre-compute the continuous contribution to the martingale self._t_prev = t0 self._continuous_martingale_at_t_prev = 1 - self._discrete_martingale = 1 + + # _discrete_martingale is a list of (time, factor) such that + # mu_d(t) is the product of all factors with time < t + self._discrete_martingale = [] if np.array_equal(cache, 'clear'): self._precomputed_continuous_martingale = {} @@ -239,7 +243,7 @@ def add_collapse(self, collapse_time, collapse_channel): rate = self._nm_solver.rate(collapse_time, collapse_channel) shift = self._nm_solver.rate_shift(collapse_time) factor = rate / (rate + shift) - self._discrete_martingale *= factor + self._discrete_martingale.append((collapse_time, factor)) def value(self, t): if self._t_prev is None: @@ -256,7 +260,13 @@ def value(self, t): self._t_prev = t self._continuous_martingale_at_t_prev = mu_c - return self._discrete_martingale * mu_c + # find value of discrete martingale at given time + mu_d = 1 + for time, factor in self._discrete_martingale: + if t > time: + mu_d *= factor + + return mu_d * mu_c def _compute_continuous_martingale(self, t1, t2): if t1 == t2: @@ -323,7 +333,6 @@ class NonMarkovianMCSolver(MCSolver): Options for the evolution. """ name = "nm_mcsolve" - _resultclass = NmmcResult solver_options = { "progress_bar": "text", "progress_kwargs": {"chunk_size": 10}, @@ -339,15 +348,12 @@ class NonMarkovianMCSolver(MCSolver): "norm_steps": 5, "norm_t_tol": 1e-6, "norm_tol": 1e-4, + "improved_sampling": False, "completeness_rtol": 1e-5, "completeness_atol": 1e-8, "martingale_quad_limit": 100, } - # both classes will be partially initialized in constructor - _trajectory_resultclass = NmmcTrajectoryResult - _mc_integrator_class = NmMCIntegrator - def __init__( self, H, ops_and_rates, args=None, options=None, ): @@ -380,14 +386,12 @@ def __init__( for op, sqrt_shifted_rate in zip(self.ops, self._sqrt_shifted_rates) ] - self._trajectory_resultclass = functools.partial( - NmmcTrajectoryResult, __nm_solver=self, - ) - self._mc_integrator_class = functools.partial( - NmMCIntegrator, __martingale=self._martingale, - ) super().__init__(H, c_ops, options=options) + @property + def _mc_integrator_class(self, *args): + return NmMCIntegrator(*args, __martingale=self._martingale) + def _check_completeness(self, ops_and_rates): """ Checks whether ``sum(Li.dag() * Li)`` is proportional to the identity @@ -509,9 +513,8 @@ def sqrt_shifted_rate(self, t, i): # the run. # # Regarding (b), in the start/step-interface we just include the martingale - # in the step method. In order to include the martingale in the - # run-interface, we use a custom trajectory-resultclass that grabs the - # martingale value from the NonMarkovianMCSolver whenever a state is added. + # in the step method. In the run-interface, the martingale is added as a + # relative weight to the trajectory result at the end of `_run_one_traj`. def start(self, state, t0, seed=None): self._martingale.initialize(t0, cache='clear') @@ -523,6 +526,16 @@ def step(self, t, *, args=None, copy=True): if isket(state): state = ket2dm(state) return state * self.current_martingale() + + def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): + """ + Run one trajectory and return the result. + """ + seed, result = super()._run_one_traj(seed, state, tlist, e_ops, + **integrator_kwargs) + martingales = [self._martingale.value(t) for t in tlist] + result.add_relative_weight(martingales) + return seed, result def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): # update `args` dictionary before precomputing martingale @@ -596,6 +609,10 @@ def options(self): norm_steps: int, default: 5 Maximum number of tries to find the collapse. + improved_sampling: Bool, default: False + Whether to use the improved sampling algorithm + of Abdelhafez et al. PRA (2019) + completeness_rtol: float, default: 1e-5 Used in determining whether the given Lindblad operators satisfy a certain completeness relation. If they do not, an additional diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 2f32dddbc9..21ce589d25 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -7,10 +7,9 @@ __all__ = [ "Result", "MultiTrajResult", + "TrajectoryResult", "McResult", "NmmcResult", - "McTrajectoryResult", - "McResultImprovedSampling", ] @@ -379,19 +378,6 @@ class MultiTrajResult(_BaseResult): Parameters ---------- - e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these - The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`.Qobj` or - :obj:`.QobjEvo` the value recorded is the expectation value of that - operator given the state at ``t``. If the element is a function, ``f``, - the value recorded is ``f(t, state)``. - - The values are recorded in the ``.expect`` attribute of this result - object. ``.expect`` is a list, where each item contains the values - of the corresponding ``e_op``. - - Function ``e_ops`` must return a number so the average can be computed. - options : dict The options for this result class. @@ -411,6 +397,14 @@ class MultiTrajResult(_BaseResult): A list of the times at which the expectation values and states were recorded. + e_ops : dict + A dictionary containing the supplied e_ops as ``ExpectOp`` instances. + The keys of the dictionary are the same as for ``.e_data``. + Each value is object where ``.e_ops[k](t, state)`` calculates the + value of ``e_op`` ``k`` at time ``t`` and the given ``state``, and + ``.e_ops[k].op`` is the original object supplied to create the + ``e_op``. + average_states : list of :obj:`.Qobj` The state at each time ``t`` (if the recording of the state was requested) averaged over all trajectories as a density matrix. @@ -497,31 +491,39 @@ class MultiTrajResult(_BaseResult): options: MultiTrajResultOptions def __init__( - self, - e_ops, - options: MultiTrajResultOptions, - *, - solver=None, - stats=None, - **kw, + self, options: MultiTrajResultOptions, *, + solver=None, stats=None, **kw, ): super().__init__(options, solver=solver, stats=stats) - self._raw_ops = self._e_ops_to_dict(e_ops) - self.times = [] self.trajectories = [] self.num_trajectories = 0 self.seeds = [] - self._sum_states = None - self._sum_final_states = None - self._sum_expect = None - self._sum2_expect = None - self._target_tols = None - self.average_e_data = {} self.std_e_data = {} - self.runs_e_data = {} + self.runs_e_data = None + + # Will be initialized at the first trajectory: + self.e_ops = None + self.times = None + + # We separate all sums into terms of trajectories with specified + # absolute weight (`_abs`) or without (`_rel`). The `_rel` will be + # initialized at the first trajectory, the `_abs` when needed + self._sum_states_abs = None + self._sum_states_rel = None + self._sum_final_states_abs = None + self._sum_final_states_rel = None + self._sum_expect_abs = None + self._sum_expect_rel = None + self._sum2_expect_abs = None + self._sum2_expect_rel = None + + # Whether we have seen any trajectories with specified absolute weight + self._abs_weights_present = False + # Number of trajectories without specified absolute weight + self._num_rel_trajectories = 0 self._post_init(**kw) @@ -529,7 +531,7 @@ def __init__( def _store_average_density_matricies(self) -> bool: return ( self.options["store_states"] - or (self.options["store_states"] is None and self._raw_ops == {}) + or (self.options["store_states"] is None and self.e_ops == {}) ) and not self.options["keep_runs_results"] @property @@ -540,68 +542,104 @@ def _store_final_density_matrix(self) -> bool: and not self.options["keep_runs_results"] ) - @staticmethod - def _to_dm(state): - if state.type == "ket": - state = state.proj() - return state - def _add_first_traj(self, trajectory): """ Read the first trajectory, intitializing needed data. """ self.times = trajectory.times + self.e_ops = trajectory.e_ops if trajectory.states and self._store_average_density_matricies: - self._sum_states = [ - qzero_like(self._to_dm(state)) for state in trajectory.states + self._sum_states_rel = [ + qzero_like(ket2dm(state)) for state in trajectory.states ] if trajectory.final_state and self._store_final_density_matrix: state = trajectory.final_state - self._sum_final_states = qzero_like(self._to_dm(state)) + self._sum_final_states_rel = qzero_like(ket2dm(state)) - self._sum_expect = [ + self._sum_expect_rel = [ np.zeros_like(expect) for expect in trajectory.expect ] - self._sum2_expect = [ + self._sum2_expect_rel = [ np.zeros_like(expect) for expect in trajectory.expect ] - self.e_ops = trajectory.e_ops - - self.average_e_data = { - k: list(avg_expect) - for k, avg_expect in zip(self._raw_ops, self._sum_expect) - } if self.options["keep_runs_results"]: - self.runs_e_data = {k: [] for k in self._raw_ops} + self.runs_e_data = {k: [] for k in self.e_ops} + + def _first_abs_trajectory(self): + if self._sum_states_rel: + self._sum_states_abs = [ + qzero_like(state) for state in self._sum_states_rel + ] + if self._sum_final_states_rel: + self._sum_final_states_abs = qzero_like(self._sum_final_states_rel) + self._sum_expect_abs = [ + np.zeros_like(expect) for expect in self._sum_expect_rel + ] + self._sum2_expect_abs = [ + np.zeros_like(expect) for expect in self._sum2_expect_rel + ] + self._abs_weights_present = True def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) def _reduce_states(self, trajectory): - self._sum_states = [ - accu + self._to_dm(state) - for accu, state in zip(self._sum_states, trajectory.states) - ] + if trajectory.has_absolute_weight(): + self._sum_states_abs = [ + accu + weight * ket2dm(state) + for accu, state, weight in zip(self._sum_states_abs, + trajectory.states, + trajectory._weight_array()) + ] + elif trajectory.has_weight(): + self._sum_states_rel = [ + accu + weight * ket2dm(state) + for accu, state, weight in zip(self._sum_states_rel, + trajectory.states, + trajectory._weight_array()) + ] + else: + self._sum_states_rel = [ + accu + ket2dm(state) + for accu, state in zip(self._sum_states_rel, trajectory.states) + ] def _reduce_final_state(self, trajectory): - self._sum_final_states += self._to_dm(trajectory.final_state) + if trajectory.has_absolute_weight(): + self._sum_final_states_abs += (trajectory._final_weight() * + ket2dm(trajectory.final_state)) + elif trajectory.has_weight(): + self._sum_final_states_rel += (trajectory._final_weight() * + ket2dm(trajectory.final_state)) + else: + self._sum_final_states_rel += ket2dm(trajectory.final_state) def _reduce_expect(self, trajectory): """ Compute the average of the expectation values and store it in it's multiple formats. """ - for i, k in enumerate(self._raw_ops): + weight = trajectory.rel_weight + if trajectory.has_absolute_weight(): + weight = weight * trajectory.abs_weight + + for i, k in enumerate(self.e_ops): expect_traj = trajectory.expect[i] - self._sum_expect[i] += expect_traj - self._sum2_expect[i] += expect_traj**2 + if trajectory.has_absolute_weight(): + self._sum_expect_abs[i] += weight * expect_traj + self._sum2_expect_abs[i] += weight * expect_traj**2 + else: + self._sum_expect_rel[i] += weight * expect_traj + self._sum2_expect_rel[i] += weight * expect_traj**2 - avg = self._sum_expect[i] / self.num_trajectories - avg2 = self._sum2_expect[i] / self.num_trajectories + avg = (self._sum_expect_abs[i] + + self._sum_expect_rel[i] / self._num_rel_trajectories) + avg2 = (self._sum2_expect_abs[i] + + self._sum2_expect_rel[i] / self._num_rel_trajectories) self.average_e_data[k] = list(avg) @@ -615,6 +653,8 @@ def _reduce_expect(self, trajectory): def _increment_traj(self, trajectory): if self.num_trajectories == 0: self._add_first_traj(trajectory) + if trajectory.has_absolute_weight() and not self._abs_weights_present: + self._first_abs_trajectory() self.num_trajectories += 1 def _no_end(self): @@ -634,8 +674,8 @@ def _fixed_end(self): return ntraj_left def _average_computer(self): - avg = np.array(self._sum_expect) / self.num_trajectories - avg2 = np.array(self._sum2_expect) / self.num_trajectories + avg = np.array(self._sum_expect_rel) / self._num_rel_trajectories + avg2 = np.array(self._sum2_expect_rel) / self._num_rel_trajectories return avg, avg2 def _target_tolerance_end(self): @@ -661,8 +701,9 @@ def _target_tolerance_end(self): return self._estimated_ntraj - self.num_trajectories def _post_init(self): - self.num_trajectories = 0 self._target_ntraj = None + self._target_tols = None + self._early_finish_check = self._no_end self.add_processor(self._increment_traj) store_trajectory = self.options["keep_runs_results"] @@ -672,10 +713,9 @@ def _post_init(self): self.add_processor(self._reduce_states) if self._store_final_density_matrix: self.add_processor(self._reduce_final_state) - if self._raw_ops: + if self.e_ops: self.add_processor(self._reduce_expect) - self._early_finish_check = self._no_end self.stats["end_condition"] = "unknown" def add(self, trajectory_info): @@ -739,7 +779,7 @@ def add_end_condition(self, ntraj, target_tol=None): self._early_finish_check = self._fixed_end return - num_e_ops = len(self._raw_ops) + num_e_ops = len(self.e_ops) if not num_e_ops: raise ValueError("Cannot target a tolerance without e_ops") @@ -779,16 +819,24 @@ def average_states(self): if self._sum_states is None: if not (self.trajectories and self.trajectories[0].states): return None - self._sum_states = [ - qzero_like(self._to_dm(state)) + self._sum_states_rel = [ + qzero_like(ket2dm(state)) for state in self.trajectories[0].states ] + if self._abs_weights_present: + self._sum_states_abs = [ + qzero_like(ket2dm(state)) + for state in self.trajectories[0].states + ] for trajectory in self.trajectories: self._reduce_states(trajectory) - return [ - final / self.num_trajectories for final in self._sum_states - ] + result = [sum_rel / self._num_rel_trajectories + for sum_rel in self._sum_states_rel] + if self._abs_weights_present: + result = [res + sum_abs + for res, sum_abs in zip(result, self._sum_states_abs)] + return result @property def states(self): @@ -816,7 +864,11 @@ def average_final_state(self): if self.average_states is not None: return self.average_states[-1] return None - return self._sum_final_states / self.num_trajectories + + result = self._sum_final_states_rel / self._num_rel_trajectories + if self._abs_weights_present: + result += self._sum_final_states_abs + return result @property def final_state(self): @@ -895,22 +947,23 @@ def __repr__(self): def __add__(self, other): if not isinstance(other, MultiTrajResult): return NotImplemented - if self._raw_ops != other._raw_ops: + if self.e_ops != other.e_ops: raise ValueError("Shared `e_ops` is required to merge results") if self.times != other.times: raise ValueError("Shared `times` are is required to merge results") new = self.__class__( - self._raw_ops, self.options, solver=self.solver, stats=self.stats + self.options, solver=self.solver, stats=self.stats ) - new.e_ops = self.e_ops if self.trajectories and other.trajectories: new.trajectories = self.trajectories + other.trajectories new.num_trajectories = self.num_trajectories + other.num_trajectories - new.times = self.times new.seeds = self.seeds + other.seeds + new.times = self.times + new.e_ops = self.e_ops + if ( self._sum_states is not None and other._sum_states is not None @@ -936,7 +989,7 @@ def __add__(self, other): new.average_e_data = {} new.std_e_data = {} - for i, k in enumerate(self._raw_ops): + for i, k in enumerate(self.e_ops): new._sum_expect.append(self._sum_expect[i] + other._sum_expect[i]) new._sum2_expect.append( self._sum2_expect[i] + other._sum2_expect[i] @@ -957,16 +1010,84 @@ def __add__(self, other): return new -class McTrajectoryResult(Result): - """ - Result class used by the :class:`.MCSolver` for single trajectories. +class TrajectoryResult(Result): + r""" + Result class used for single trajectories in multi-trajectory simulations. + + A trajectory may come with a weight. The trajectory average of an + observable O is then performed as + + .. math:: + \langle O \rangle = \sum_k w(k) O(k) , + + where O is an observable, w(k) the weight of the k-th trajectory, and O(k) + the observable on the k-th trajectory. The weight may be time-dependent. + + There may be an absolute weight `wa` and / or a relative weight `wr`. + The total weight is `w = wa * wr` if the absolute weight is set, and + `w = wr / N` otherwise (where N is the number of trajectories with no + absolute weight specified). + + Attributes + ---------- + rel_weight: float or list + The relative weight, constant or time-dependent. + + abs_weight: float or list or None + The absolute weight, constant or time-dependent. + None if no absolute weight has been set. """ - def __init__(self, e_ops, options, *args, **kwargs): - super().__init__( - e_ops, {**options, "normalize_output": False}, *args, **kwargs - ) + def _post_init(self): + super()._post_init() + + self.rel_weight = np.array(1) + self.abs_weight = None + self._has_weight = False + + def add_absolute_weight(self, new_weight): + """ + Adds the given weight (which may be either a number or an array of the + same length as the list of times) as an absolute weight. + """ + new_weight = np.array(new_weight) + if self.abs_weight is None: + self.abs_weight = new_weight + else: + self.abs_weight = self.abs_weight * new_weight + self._has_weight = True + + def add_relative_weight(self, new_weight): + """ + Adds the given weight (which may be either a number or an array of the + same length as the list of times) as a relative weight. + """ + new_weight = np.array(new_weight) + self.rel_weight = self.rel_weight * new_weight + self._has_weight = True + + def has_weight(self): + """Whether any weight has been set.""" + return self._has_weight + + def has_absolute_weight(self): + """Whether an absolute weight has been set.""" + return (self.abs_weight is not None) + def _weight_array(self): + """ + Returns an array containing the weight as a function of time. If no + absolute weight was set, this is only the relative weight. If an + absolute weight was set, this is the product of abs and rel. + """ + weights = np.ones_like(self.times) + weights = weights * self.rel_weight + if self.abs_weight: + weights = weights * self.abs_weight + return weights + + def _final_weight(self): + return self._weight_array()[-1] class McResult(MultiTrajResult): """ @@ -1078,6 +1199,108 @@ def runs_photocurrent(self): ) return measurements + +class NmmcResult(McResult): + """ + Class for storing the results of the non-Markovian Monte-Carlo solver. + + Parameters + ---------- + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these + The ``e_ops`` parameter defines the set of values to record at + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that + operator given the state at ``t``. If the element is a function, ``f``, + the value recorded is ``f(t, state)``. + + The values are recorded in the ``.expect`` attribute of this result + object. ``.expect`` is a list, where each item contains the values + of the corresponding ``e_op``. + + options : :obj:`~SolverResultsOptions` + The options for this result class. + + solver : str or None + The name of the solver generating these results. + + stats : dict + The stats generated by the solver while producing these results. Note + that the solver may update the stats directly while producing results. + Must include a value for "num_collapse". + + kw : dict + Additional parameters specific to a result sub-class. + + Attributes + ---------- + average_trace : list + The average trace (i.e., averaged over all trajectories) at each time. + + std_trace : list + The standard deviation of the trace at each time. + + runs_trace : list of lists + For each recorded trajectory, the trace at each time. + Only present if ``keep_runs_results`` is set in the options. + """ + + def _post_init(self): + super()._post_init() + + self._sum_trace = None + self._sum2_trace = None + self.average_trace = [] + self.std_trace = [] + self.runs_trace = [] + + self.add_processor(self._add_trace) + + def _add_first_traj(self, trajectory): + super()._add_first_traj(trajectory) + self._sum_trace = np.zeros_like(trajectory.times) + self._sum2_trace = np.zeros_like(trajectory.times) + + def _add_trace(self, trajectory): + new_trace = np.array(trajectory.trace) + self._sum_trace += new_trace + self._sum2_trace += np.abs(new_trace) ** 2 + + avg = self._sum_trace / self.num_trajectories + avg2 = self._sum2_trace / self.num_trajectories + + self.average_trace = avg + self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) + + if self.options["keep_runs_results"]: + self.runs_trace.append(trajectory.trace) + + @property + def trace(self): + """ + Refers to ``average_trace`` or ``runs_trace``, depending on whether + ``keep_runs_results`` is set in the options. + """ + return self.runs_trace or self.average_trace + + + + + + + + + + + + + + + + + + + + class McResultImprovedSampling(McResult, MultiTrajResult): """ @@ -1104,21 +1327,21 @@ def __init__(self, e_ops, options, **kw): def _reduce_states(self, trajectory): if self.num_trajectories == 1: self._sum_states_no_jump = [ - accu + self._to_dm(state) + accu + ket2dm(state) for accu, state in zip( self._sum_states_no_jump, trajectory.states ) ] else: self._sum_states_jump = [ - accu + self._to_dm(state) + accu + ket2dm(state) for accu, state in zip( self._sum_states_jump, trajectory.states ) ] def _reduce_final_state(self, trajectory): - dm_final_state = self._to_dm(trajectory.final_state) + dm_final_state = ket2dm(trajectory.final_state) if self.num_trajectories == 1: self._sum_final_states_no_jump += dm_final_state else: @@ -1134,16 +1357,16 @@ def _add_first_traj(self, trajectory): if trajectory.states and self._store_average_density_matricies: del self._sum_states self._sum_states_no_jump = [ - qzero_like(self._to_dm(state)) for state in trajectory.states + qzero_like(ket2dm(state)) for state in trajectory.states ] self._sum_states_jump = [ - qzero_like(self._to_dm(state)) for state in trajectory.states + qzero_like(ket2dm(state)) for state in trajectory.states ] if trajectory.final_state and self._store_final_density_matrix: state = trajectory.final_state del self._sum_final_states - self._sum_final_states_no_jump = qzero_like(self._to_dm(state)) - self._sum_final_states_jump = qzero_like(self._to_dm(state)) + self._sum_final_states_no_jump = qzero_like(ket2dm(state)) + self._sum_final_states_jump = qzero_like(ket2dm(state)) self._sum_expect_jump = [ np.zeros_like(expect) for expect in trajectory.expect ] @@ -1208,11 +1431,11 @@ def average_states(self): if not (self.trajectories and self.trajectories[0].states): return None self._sum_states_no_jump = [ - qzero_like(self._to_dm(state)) + qzero_like(ket2dm(state)) for state in self.trajectories[0].states ] self._sum_states_jump = [ - qzero_like(self._to_dm(state)) + qzero_like(ket2dm(state)) for state in self.trajectories[0].states ] self.num_trajectories = 0 @@ -1263,112 +1486,3 @@ def photocurrent(self): for i in range(self.num_c_ops) ] return mesurement - - -class NmmcTrajectoryResult(McTrajectoryResult): - """ - Result class used by the :class:`.NonMarkovianMCSolver` for single - trajectories. Additionally stores the trace of the state along the - trajectory. - """ - - def __init__(self, e_ops, options, *args, **kwargs): - self._nm_solver = kwargs.pop("__nm_solver") - super().__init__(e_ops, options, *args, **kwargs) - self.trace = [] - - # This gets called during the Monte-Carlo simulation of the associated - # completely positive master equation. To obtain the state of the actual - # system, we simply multiply the provided state with the current martingale - # before storing it / computing expectation values. - def add(self, t, state): - if isket(state): - state = ket2dm(state) - mu = self._nm_solver.current_martingale() - super().add(t, state * mu) - self.trace.append(mu) - - add.__doc__ = Result.add.__doc__ - - -class NmmcResult(McResult): - """ - Class for storing the results of the non-Markovian Monte-Carlo solver. - - Parameters - ---------- - e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these - The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`.Qobj` or - :obj:`.QobjEvo` the value recorded is the expectation value of that - operator given the state at ``t``. If the element is a function, ``f``, - the value recorded is ``f(t, state)``. - - The values are recorded in the ``.expect`` attribute of this result - object. ``.expect`` is a list, where each item contains the values - of the corresponding ``e_op``. - - options : :obj:`~SolverResultsOptions` - The options for this result class. - - solver : str or None - The name of the solver generating these results. - - stats : dict - The stats generated by the solver while producing these results. Note - that the solver may update the stats directly while producing results. - Must include a value for "num_collapse". - - kw : dict - Additional parameters specific to a result sub-class. - - Attributes - ---------- - average_trace : list - The average trace (i.e., averaged over all trajectories) at each time. - - std_trace : list - The standard deviation of the trace at each time. - - runs_trace : list of lists - For each recorded trajectory, the trace at each time. - Only present if ``keep_runs_results`` is set in the options. - """ - - def _post_init(self): - super()._post_init() - - self._sum_trace = None - self._sum2_trace = None - self.average_trace = [] - self.std_trace = [] - self.runs_trace = [] - - self.add_processor(self._add_trace) - - def _add_first_traj(self, trajectory): - super()._add_first_traj(trajectory) - self._sum_trace = np.zeros_like(trajectory.times) - self._sum2_trace = np.zeros_like(trajectory.times) - - def _add_trace(self, trajectory): - new_trace = np.array(trajectory.trace) - self._sum_trace += new_trace - self._sum2_trace += np.abs(new_trace) ** 2 - - avg = self._sum_trace / self.num_trajectories - avg2 = self._sum2_trace / self.num_trajectories - - self.average_trace = avg - self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) - - if self.options["keep_runs_results"]: - self.runs_trace.append(trajectory.trace) - - @property - def trace(self): - """ - Refers to ``average_trace`` or ``runs_trace``, depending on whether - ``keep_runs_results`` is set in the options. - """ - return self.runs_trace or self.average_trace From 433bb721076b33b06e33039d8fd94c0d8cda9c65 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Sun, 24 Mar 2024 13:09:35 +0900 Subject: [PATCH 041/305] stochatic -> stochastic --- qutip/solver/stochastic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index af57fe40a1..e999e2098a 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -232,14 +232,14 @@ def arguments(self, args): sc_op.arguments(args) def _register_feedback(self, val): - self.H._register_feedback({"wiener_process": val}, "stochatic solver") + self.H._register_feedback({"wiener_process": val}, "stochastic solver") for c_op in self.c_ops: c_op._register_feedback( - {"WienerFeedback": val}, "stochatic solver" + {"WienerFeedback": val}, "stochastic solver" ) for sc_op in self.sc_ops: sc_op._register_feedback( - {"WienerFeedback": val}, "stochatic solver" + {"WienerFeedback": val}, "stochastic solver" ) From 71a8ec432aa83caacc00daf73e384d28b6558208 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Sun, 24 Mar 2024 20:43:19 +0900 Subject: [PATCH 042/305] Reduced code duplication in stochastic solvers --- qutip/solver/mcsolve.py | 11 ++------- qutip/solver/multitraj.py | 12 ++++----- qutip/solver/stochastic.py | 50 ++++++++++++++------------------------ 3 files changed, 25 insertions(+), 48 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index e2d2f87342..0d4200151e 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -181,13 +181,6 @@ def __init__(self, rhs, c_ops, n_ops): def __call__(self): return self.rhs - def __getattr__(self, attr): - if attr == "rhs": - raise AttributeError - if hasattr(self.rhs, attr): - return getattr(self.rhs, attr) - raise AttributeError - def arguments(self, args): self.rhs.arguments(args) for c_op in self.c_ops: @@ -539,9 +532,9 @@ def _get_integrator(self): integrator = method else: raise ValueError("Integrator method not supported.") - integrator_instance = integrator(self.system(), self.options) + integrator_instance = integrator(self.rhs(), self.options) mc_integrator = self._mc_integrator_class( - integrator_instance, self.system, self.options + integrator_instance, self.rhs, self.options ) self._init_integrator_time = time() - _time_start return mc_integrator diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 16ce13bec8..b2bf947ff5 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -15,9 +15,6 @@ class _MTSystem: def __init__(self, rhs): self.rhs = rhs - def __call__(self): - return self.rhs - def arguments(self, args): self.rhs.arguments(args) @@ -25,6 +22,8 @@ def _register_feedback(self, type, val): pass def __getattr__(self, attr): + if attr == "rhs": + raise AttributeError if hasattr(self.rhs, attr): return getattr(self.rhs, attr) raise AttributeError @@ -71,12 +70,11 @@ class MultiTrajSolver(Solver): def __init__(self, rhs, *, options=None): if isinstance(rhs, QobjEvo): - self.system = _MTSystem(rhs) + self.rhs = _MTSystem(rhs) elif isinstance(rhs, _MTSystem): - self.system = rhs + self.rhs = rhs else: raise TypeError("The system should be a QobjEvo") - self.rhs = self.system() self.options = options self.seed_sequence = np.random.SeedSequence() self._integrator = self._get_integrator() @@ -280,7 +278,7 @@ def _read_seed(self, seed, ntraj): def _argument(self, args): """Update the args, for the `rhs` and `c_ops` and other operators.""" if args: - self.system.arguments(args) + self.rhs.arguments(args) self._integrator.arguments(args) def _get_generator(self, seed): diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index f39837c956..3f74207396 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -2,7 +2,7 @@ from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .result import MultiTrajResult, Result, ExpectOp -from .multitraj import MultiTrajSolver +from .multitraj import _MTSystem, MultiTrajSolver from .. import Qobj, QobjEvo from ..core.dimensions import Dimensions import numpy as np @@ -26,7 +26,7 @@ def _post_init(self, m_ops=(), dw_factor=(), heterodyne=False): self.m_ops.append(ExpectOp(op, f, self.m_expect[-1].append)) self.add_processor(self.m_ops[-1]._store) - def add(self, t, state, noise): + def add(self, t, state, noise=None): super().add(t, state) if noise is not None and self.options["store_measurement"]: for i, dW in enumerate(noise): @@ -166,7 +166,7 @@ def wiener_process(self): return self._trajectories_attr("wiener_process") -class _StochasticRHS: +class _StochasticRHS(_MTSystem): """ In between object to store the stochastic system. @@ -181,7 +181,7 @@ class _StochasticRHS: def __init__(self, issuper, H, sc_ops, c_ops, heterodyne): if not isinstance(H, (Qobj, QobjEvo)) or not H.isoper: - raise TypeError("The Hamiltonian must be am operator") + raise TypeError("The Hamiltonian must be an operator") self.H = QobjEvo(H) if isinstance(sc_ops, (Qobj, QobjEvo)): @@ -500,8 +500,8 @@ class StochasticSolver(MultiTrajSolver): name = "StochasticSolver" _resultclass = StochasticResult _avail_integrators = {} - system = None _open = None + solver_options = { "progress_bar": "text", "progress_kwargs": {"chunk_size": 10}, @@ -517,20 +517,22 @@ class StochasticSolver(MultiTrajSolver): "store_measurement": False, } + def _trajectory_resultclass(self, e_ops, options): + return StochasticTrajResult( + e_ops, + options, + m_ops=self.m_ops, + dw_factor=self.dW_factors, + heterodyne=self.heterodyne, + ) + def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): - self.options = options self._heterodyne = heterodyne if self.name == "ssesolve" and c_ops: raise ValueError("c_ops are not supported by ssesolve.") rhs = _StochasticRHS(self._open, H, sc_ops, c_ops, heterodyne) - self.rhs = rhs - self.system = rhs - self.options = options - self.seed_sequence = np.random.SeedSequence() - self._integrator = self._get_integrator() - self._state_metadata = {} - self.stats = self._initialize_stats() + super().__init__(rhs, options=options) if heterodyne: self._m_ops = [] @@ -619,25 +621,9 @@ def dW_factors(self, new_dW_factors): ) self._dW_factors = new_dW_factors - def _run_one_traj(self, seed, state, tlist, e_ops): - """ - Run one trajectory and return the result. - """ - result = StochasticTrajResult( - e_ops, - self.options, - m_ops=self.m_ops, - dw_factor=self.dW_factors, - heterodyne=self.heterodyne, - ) - generator = self._get_generator(seed) - self._integrator.set_state(tlist[0], state, generator) - state_t = self._restore_state(state, copy=False) - result.add(tlist[0], state_t, None) - for t in tlist[1:]: - t, state, noise = self._integrator.integrate(t, copy=False) - state_t = self._restore_state(state, copy=False) - result.add(t, state_t, noise) + def _integrate_one_traj(self, seed, tlist, result): + for t, state, noise in self._integrator.run(tlist): + result.add(t, self._restore_state(state, copy=False), noise) return seed, result @classmethod From 56c89d4be685cbac27d8fe3ee04680e4150f82b8 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Sun, 24 Mar 2024 21:30:05 +0900 Subject: [PATCH 043/305] trajectory solvers: rename system -> rhs --- qutip/solver/mcsolve.py | 13 +++++++------ qutip/solver/multitraj.py | 6 +++--- qutip/solver/stochastic.py | 4 ++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 0d4200151e..40f603fff7 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -2,7 +2,7 @@ import numpy as np from ..core import QobjEvo, spre, spost, Qobj, unstack_columns -from .multitraj import MultiTrajSolver, _MTSystem +from .multitraj import MultiTrajSolver, _MultiTrajRHS from .solver_base import Solver, Integrator, _solver_deprecation from .result import McResult, McTrajectoryResult, McResultImprovedSampling from .mesolve import mesolve, MESolver @@ -167,16 +167,17 @@ def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, return result -class _MCSystem(_MTSystem): +class _MCRHS(_MultiTrajRHS): """ Container for the operators of the solver. """ - def __init__(self, rhs, c_ops, n_ops): - self.rhs = rhs + def __init__(self, H, c_ops, n_ops): + self.rhs = H + + self.H = H self.c_ops = c_ops self.n_ops = n_ops - self._collapse_key = "" def __call__(self): return self.rhs @@ -445,7 +446,7 @@ def __init__(self, H, c_ops, *, options=None): self._num_collapse = len(self._c_ops) self.options = options - system = _MCSystem(rhs, self._c_ops, self._n_ops) + system = _MCRHS(rhs, self._c_ops, self._n_ops) super().__init__(system, options=options) def _restore_state(self, data, *, copy=True): diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index b2bf947ff5..3d1cf6ee95 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -8,7 +8,7 @@ __all__ = ["MultiTrajSolver"] -class _MTSystem: +class _MultiTrajRHS: """ Container for the operators of the solver. """ @@ -70,8 +70,8 @@ class MultiTrajSolver(Solver): def __init__(self, rhs, *, options=None): if isinstance(rhs, QobjEvo): - self.rhs = _MTSystem(rhs) - elif isinstance(rhs, _MTSystem): + self.rhs = _MultiTrajRHS(rhs) + elif isinstance(rhs, _MultiTrajRHS): self.rhs = rhs else: raise TypeError("The system should be a QobjEvo") diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 3f74207396..d87947c147 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -2,7 +2,7 @@ from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .result import MultiTrajResult, Result, ExpectOp -from .multitraj import _MTSystem, MultiTrajSolver +from .multitraj import _MultiTrajRHS, MultiTrajSolver from .. import Qobj, QobjEvo from ..core.dimensions import Dimensions import numpy as np @@ -166,7 +166,7 @@ def wiener_process(self): return self._trajectories_attr("wiener_process") -class _StochasticRHS(_MTSystem): +class _StochasticRHS(_MultiTrajRHS): """ In between object to store the stochastic system. From a45addefd5111cc74762e9f6415ddc692215e171 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Sun, 24 Mar 2024 21:50:29 +0900 Subject: [PATCH 044/305] Use H in monte carlo rhs --- qutip/solver/mcsolve.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 40f603fff7..ca9802f98b 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -180,17 +180,17 @@ def __init__(self, H, c_ops, n_ops): self.n_ops = n_ops def __call__(self): - return self.rhs + return self.H def arguments(self, args): - self.rhs.arguments(args) + self.H.arguments(args) for c_op in self.c_ops: c_op.arguments(args) for n_op in self.n_ops: n_op.arguments(args) def _register_feedback(self, key, val): - self.rhs._register_feedback({key: val}, solver="McSolver") + self.H._register_feedback({key: val}, solver="McSolver") for c_op in self.c_ops: c_op._register_feedback({key: val}, solver="McSolver") for n_op in self.n_ops: From 1ea5a127214f68b3a7859d6e5cb78ed0eeaa94a1 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 25 Mar 2024 00:03:55 +0900 Subject: [PATCH 045/305] WIP commit 2: weighted trajectories --- qutip/solver/result.py | 66 +++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 2bc3eb15f8..244b4b1ff9 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -816,7 +816,7 @@ def average_states(self): """ States averages as density matrices. """ - if self._sum_states is None: + if self._sum_states_rel is None: if not (self.trajectories and self.trajectories[0].states): return None self._sum_states_rel = [ @@ -860,7 +860,7 @@ def average_final_state(self): """ Last states of each trajectories averaged into a density matrix. """ - if self._sum_final_states is None: + if self._sum_final_states_rel is None: if self.average_states is not None: return self.average_states[-1] return None @@ -946,7 +946,7 @@ def __repr__(self): def __add__(self, other): if not isinstance(other, MultiTrajResult): - return NotImplemented + raise NotImplementedError("Can only add two multi traj results") if self.e_ops != other.e_ops: raise ValueError("Shared `e_ops` is required to merge results") if self.times != other.times: @@ -955,44 +955,64 @@ def __add__(self, other): new = self.__class__( self.options, solver=self.solver, stats=self.stats ) + new.times = self.times + new.e_ops = self.e_ops if self.trajectories and other.trajectories: new.trajectories = self.trajectories + other.trajectories new.num_trajectories = self.num_trajectories + other.num_trajectories new.seeds = self.seeds + other.seeds - new.times = self.times - new.e_ops = self.e_ops + new._num_rel_trajectories = (self._num_rel_trajectories + + other._num_rel_trajectories) + new._abs_weights_present = (self._abs_weights_present or + other._abs_weights_present) + if new._abs_weights_present: + new._first_abs_trajectory() # initialize abs fields + #TODO shit this only works after all the "_rel" are initialized... if ( - self._sum_states is not None - and other._sum_states is not None + self._sum_states_rel is not None + and other._sum_states_rel is not None ): - new._sum_states = [ + new._sum_states_rel = [ state1 + state2 for state1, state2 in zip( - self._sum_states, other._sum_states + self._sum_states_rel, other._sum_states_rel ) ] + if self._abs_weights_present: + new._sum_states_abs = list(self._sum_states_abs) + if other._abs_weights_present: + new._sum_states_abs = [ + state1 + state2 for state1, state2 in zip( + new._sum_states_abs, other._sum_states_abs + ) + ] if ( - self._sum_final_states is not None - and other._sum_final_states is not None + self._sum_final_states_rel is not None + and other._sum_final_states_rel is not None ): - new._sum_final_states = ( - self._sum_final_states - + other._sum_final_states - ) - new._target_tols = None + new._sum_final_states_rel = (self._sum_final_states_rel + + other._sum_final_states_rel) + if self._abs_weights_present: + new._sum_final_states_abs = self._sum_final_states_abs + if other._abs_weights_present: + new._sum_final_states_abs += other._sum_final_states_abs - new._sum_expect = [] - new._sum2_expect = [] - new.average_e_data = {} - new.std_e_data = {} + new._sum_expect_rel = [] + new._sum2_expect_rel = [] + if self._abs_weights_present: + new._sum_expect_abs = [] + new._sum2_expect_abs = [] + if self.runs_e_data and other.runs_e_data: + new.runs_e_data = {} for i, k in enumerate(self.e_ops): - new._sum_expect.append(self._sum_expect[i] + other._sum_expect[i]) - new._sum2_expect.append( - self._sum2_expect[i] + other._sum2_expect[i] + new._sum_expect_rel.append( + self._sum_expect_rel[i] + other._sum_expect_rel[i]) + new._sum2_expect_rel.append( + self._sum2_expect_rel[i] + other._sum2_expect_rel[i] ) avg = new._sum_expect[i] / new.num_trajectories From 95e790b87ab792123266fa5ca2bd961f7fddc0b9 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 25 Mar 2024 15:21:15 +0900 Subject: [PATCH 046/305] Implemented suggestions by @Ericgig --- qutip/solver/mcsolve.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index ca9802f98b..43dc60893f 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -174,23 +174,21 @@ class _MCRHS(_MultiTrajRHS): def __init__(self, H, c_ops, n_ops): self.rhs = H - - self.H = H self.c_ops = c_ops self.n_ops = n_ops def __call__(self): - return self.H + return self.rhs def arguments(self, args): - self.H.arguments(args) + self.rhs.arguments(args) for c_op in self.c_ops: c_op.arguments(args) for n_op in self.n_ops: n_op.arguments(args) def _register_feedback(self, key, val): - self.H._register_feedback({key: val}, solver="McSolver") + self.rhs._register_feedback({key: val}, solver="McSolver") for c_op in self.c_ops: c_op._register_feedback({key: val}, solver="McSolver") for n_op in self.n_ops: @@ -367,6 +365,10 @@ def _do_collapse(self, collapse_time, state): def arguments(self, args): if args: self._integrator.arguments(args) + for c_op in self._c_ops: + c_op.arguments(args) + for n_op in self._n_ops: + n_op.arguments(args) @property def integrator_options(self): From 220c39c37b8b32bed040bc30636e9eaece90d9e9 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 25 Mar 2024 07:44:11 +0900 Subject: [PATCH 047/305] Install without build isolation --- .github/workflows/tests.yml | 36 +++++++++++++++++++++++++----------- qutip/tests/test_scipy.py | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 qutip/tests/test_scipy.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c046f02a4e..6453c5dda8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,6 +69,7 @@ jobs: python-version: "3.10" scipy-requirement: ">=1.10,<1.11" numpy-requirement: ">=1.24,<1.25" + oldcython: 1 nocython: 1 # Python 3.11 and recent numpy @@ -128,15 +129,19 @@ jobs: # In the run, first we handle any special cases. We do this in bash # rather than in the GitHub Actions file directly, because bash gives us # a proper programming language to use. + # We install without build isolation so qutip is compiled with the + # version of cython, scipy, numpy in the test matrix, no a temporary + # version use in the installation virtual environment. run: | - QUTIP_TARGET="tests,graphics,semidefinite,ipython,extras" - if [[ -z "${{ matrix.nocython }}" ]]; then - QUTIP_TARGET="$QUTIP_TARGET,runtime_compilation" - fi - if [[ "${{ matrix.oldcython }}" ]]; then - pip install cython==0.29.36 - fi - export CI_QUTIP_WITH_OPENMP=${{ matrix.openmp }} + # Install the extra requirement + python -m pip install pytest>=5.2 pytest-rerunfailures # tests + python -m pip install matplotlib>=1.2.1 # graphics + python -m pip install cvxpy>=1.0 cvxopt # semidefinite + python -m pip install ipython # ipython + python -m pip install loky tqdm # extras + python -m pip install "coverage${{ matrix.coverage-requirement }}" chardet + python -m pip install pytest-cov coveralls pytest-fail-slow + if [[ -z "${{ matrix.nomkl }}" ]]; then conda install blas=*=mkl "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" elif [[ "${{ matrix.os }}" =~ ^windows.*$ ]]; then @@ -153,9 +158,17 @@ jobs: # Use openmpi because mpich causes problems. Note, environment variable names change in v5 conda install "openmpi<5" mpi4py fi - python -m pip install -e .[$QUTIP_TARGET] - python -m pip install "coverage${{ matrix.coverage-requirement }}" - python -m pip install pytest-cov coveralls pytest-fail-slow + if [[ "${{ matrix.oldcython }}" ]]; then + python -m pip install cython==0.29.36 filelock + else + python -m pip install cython filelock + fi + + python -m pip install -e . -v --no-build-isolation + + if [[ "${{ matrix.nocython }}" ]]; then + python -m pip uninstall cython -y + fi - name: Package information run: | @@ -190,6 +203,7 @@ jobs: # We only have 2 physical cores, but we want to test mpi_pmap with 2 workers. export OMPI_MCA_rmaps_base_oversubscribe=true fi + pytest qutip/tests -k test_scipy pytest -Werror --strict-config --strict-markers --fail-slow=300 --durations=0 --durations-min=1.0 --verbosity=1 --cov=qutip --cov-report= --color=yes ${{ matrix.pytest-extra-options }} qutip/tests # Above flags are: # -Werror diff --git a/qutip/tests/test_scipy.py b/qutip/tests/test_scipy.py new file mode 100644 index 0000000000..81b3a541b6 --- /dev/null +++ b/qutip/tests/test_scipy.py @@ -0,0 +1,21 @@ +import numpy as np +import scipy +import pytest + +@pytest.mark.parametrize("N", [10, 100]) +def test_sparse_eigen(N): + import scipy.sparse + matrix = scipy.sparse.random(N, N, format="csr", dtype=np.double) + out = scipy.sparse.linalg.eigs(matrix, 4) + +@pytest.mark.parametrize("N", [10, 100]) +def test_sparse_eigen(N): + import scipy.sparse + matrix = scipy.sparse.random(N, N, format="csr", dtype=np.complex128) + out = scipy.sparse.linalg.eigs(matrix, 4) + +@pytest.mark.parametrize("N", [10, 100]) +def test_sparse_svd(N): + import scipy.sparse + matrix = scipy.sparse.random(N, N, format="csr", dtype=np.complex128) + out = scipy.sparse.linalg.svds(matrix, 4) From 2079d8f70cf98ed65a72cd3c98b533803de7fe17 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 26 Mar 2024 09:54:22 +0900 Subject: [PATCH 048/305] Remove test scipy --- .github/workflows/tests.yml | 1 - qutip/tests/test_scipy.py | 21 --------------------- 2 files changed, 22 deletions(-) delete mode 100644 qutip/tests/test_scipy.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6453c5dda8..07f3472143 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -203,7 +203,6 @@ jobs: # We only have 2 physical cores, but we want to test mpi_pmap with 2 workers. export OMPI_MCA_rmaps_base_oversubscribe=true fi - pytest qutip/tests -k test_scipy pytest -Werror --strict-config --strict-markers --fail-slow=300 --durations=0 --durations-min=1.0 --verbosity=1 --cov=qutip --cov-report= --color=yes ${{ matrix.pytest-extra-options }} qutip/tests # Above flags are: # -Werror diff --git a/qutip/tests/test_scipy.py b/qutip/tests/test_scipy.py deleted file mode 100644 index 81b3a541b6..0000000000 --- a/qutip/tests/test_scipy.py +++ /dev/null @@ -1,21 +0,0 @@ -import numpy as np -import scipy -import pytest - -@pytest.mark.parametrize("N", [10, 100]) -def test_sparse_eigen(N): - import scipy.sparse - matrix = scipy.sparse.random(N, N, format="csr", dtype=np.double) - out = scipy.sparse.linalg.eigs(matrix, 4) - -@pytest.mark.parametrize("N", [10, 100]) -def test_sparse_eigen(N): - import scipy.sparse - matrix = scipy.sparse.random(N, N, format="csr", dtype=np.complex128) - out = scipy.sparse.linalg.eigs(matrix, 4) - -@pytest.mark.parametrize("N", [10, 100]) -def test_sparse_svd(N): - import scipy.sparse - matrix = scipy.sparse.random(N, N, format="csr", dtype=np.complex128) - out = scipy.sparse.linalg.svds(matrix, 4) From 0d6b29b81b1907fe571d7fd7e0211e5690005e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Tue, 26 Mar 2024 00:03:33 -0400 Subject: [PATCH 049/305] Update .github/workflows/tests.yml Co-authored-by: Simon Cross --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 07f3472143..f758c46f1d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -130,7 +130,7 @@ jobs: # rather than in the GitHub Actions file directly, because bash gives us # a proper programming language to use. # We install without build isolation so qutip is compiled with the - # version of cython, scipy, numpy in the test matrix, no a temporary + # version of cython, scipy, numpy in the test matrix, not a temporary # version use in the installation virtual environment. run: | # Install the extra requirement From ee9be66e6bb5787629424ff72417a71246db37f6 Mon Sep 17 00:00:00 2001 From: Andrey Rakhubovsky Date: Tue, 26 Mar 2024 16:42:50 +0100 Subject: [PATCH 050/305] Fix argument type in doctring of projection() In projection(dim, m, n, ...), m,n are Fock state numbers, so must be integers. Also matches docstring of basis() --- qutip/core/states.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/states.py b/qutip/core/states.py index 2075dfe7b6..2ca6754d50 100644 --- a/qutip/core/states.py +++ b/qutip/core/states.py @@ -571,7 +571,7 @@ def projection(dimensions, n, m, offset=None, *, dtype=None): Number of basis states in Hilbert space. If a list, then the resultant object will be a tensor product over spaces with those dimensions. - n, m : float + n, m : int The number states in the projection. offset : int, default: 0 From fe87a70156d9c3fc3d1dbc1f27ba43525de2c055 Mon Sep 17 00:00:00 2001 From: Andrey Rakhubovsky Date: Tue, 26 Mar 2024 16:50:15 +0100 Subject: [PATCH 051/305] add towncrier --- doc/changes/2363.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2363.doc diff --git a/doc/changes/2363.doc b/doc/changes/2363.doc new file mode 100644 index 0000000000..ed00922a62 --- /dev/null +++ b/doc/changes/2363.doc @@ -0,0 +1 @@ +Add your info here \ No newline at end of file From 65cc3ebc8fd748b753ea2a14d4091ccc204d43fa Mon Sep 17 00:00:00 2001 From: Andrey Rakhubovsky Date: Tue, 26 Mar 2024 16:52:19 +0100 Subject: [PATCH 052/305] amend towncrier --- doc/changes/2363.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/2363.doc b/doc/changes/2363.doc index ed00922a62..981f95c320 100644 --- a/doc/changes/2363.doc +++ b/doc/changes/2363.doc @@ -1 +1 @@ -Add your info here \ No newline at end of file +Fix types in docstring of projection() From 8eb9ce3f51ce4603565b971ba15a75f5006f61ce Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 27 Mar 2024 07:36:31 +0900 Subject: [PATCH 053/305] Add v5 changelog --- doc/changelog.rst | 457 ++++++++++++++++++++++++++++++++++++++- doc/changes/2352.feature | 1 - doc/changes/2354.misc | 1 - 3 files changed, 456 insertions(+), 3 deletions(-) delete mode 100644 doc/changes/2352.feature delete mode 100644 doc/changes/2354.misc diff --git a/doc/changelog.rst b/doc/changelog.rst index a916a657c5..18f0a93a33 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,6 +6,461 @@ Change Log .. towncrier release notes start + +QuTiP 5.0.0 (2024-03-26) +======================== + + +QuTiP 5 is a redesign of many of the core components of QuTiP (``Qobj``, +``QobjEvo``, solvers) to make them more consistent and more flexible. + +``Qobj`` may now be stored in either sparse or dense representations, +and the two may be mixed sensibly as needed. ``QobjEvo`` is now used +consistently throughout QuTiP, and the implementation has been +substantially cleaned up. A new ``Coefficient`` class is used to +represent the time-dependent factors inside ``QobjEvo``. + +The solvers have been rewritten to work well with the new data layer +and the concept of ``Integrators`` which solve ODEs has been introduced. +In future, new data layers may provide their own ``Integrators`` +specialized to their representation of the underlying data. + +Much of the user-facing API of QuTiP remains familiar, but there have +had to be many small breaking changes. If we can make changes to +easy migrating code from QuTiP 4 to QuTiP 5, please let us know. + +Any extensive list of changes follows. + +Contributors +------------ + +QuTiP 5 has been a large effort by many people over the last three years. + +In particular: + +- Jake Lishman led the implementation of the new data layer and coefficients. +- Eric Giguère led the implementation of the new QobjEvo interface and solvers. +- Boxi Li led the updating of QuTiP's QIP support and the creation of ``qutip_qip``. + +Other members of the QuTiP Admin team have been heavily involved in reviewing, +testing and designing QuTiP 5: + +- Alexander Pitchford +- Asier Galicia +- Nathan Shammah +- Shahnawaz Ahmed +- Neill Lambert +- Simon Cross +- Paul Menczel + +Two Google Summer of Code contributors updated the tutorials and benchmarks to +QuTiP 5: + +- Christian Staufenbiel updated many of the tutorials (``). +- Xavier Sproken update the benchmarks (``). + +During an internship at RIKEN, Patrick Hopf created new quantum control method and +improved the existing methods interface: + +- Patrick Hopf created new quantum control package (``). + +Four experimental data layers backends were written either as part of Google Summer +of Code or as separate projects. While these are still alpha quality, the helped +significantly to test the data layer API: + +- ``qutip-tensorflow``: a TensorFlow backend by Asier Galicia (``) +- ``qutip-cupy``: a CuPy GPU backend by Felipe Bivort Haiek (``)` +- ``qutip-tensornetwork``: a TensorNetwork backend by Asier Galicia (``) +- ``qutip-jax``: a JAX backend by Eric Giguère (``) + +Finally, Yuji Tamakoshi updated the visualization function and added animation +functions as part of Google Summer of Code project. + +We have also had many other contributors, whose specific contributions are +detailed below: + +- Pieter Eendebak (updated the required SciPy to 1.5+, `#1982 `). +- Pieter Eendebak (reduced import times by setting logger names, `#1981 `) +- Pieter Eendebak (Allow scipy 1.12 to be used with qutip, `#2354 `) +- Xavier Sproken (included C header files in the source distribution, `#1971 `) +- Christian Staufenbiel (added support for multiple collapse operators to the Floquet solver, `#1962 `) +- Christian Staufenbiel (fixed the basis used in the Floquet Master Equation solver, `#1952 `) +- Christian Staufenbiel (allowed the ``bloch_redfield_tensor`` function to accept strings and callables for `a_ops`, `#1951 `) +- Christian Staufenbiel (Add a guide on Superoperators, Pauli Basis and Channel Contraction, `#1984 `) +- Henrique Silvéro (allowed ``qutip_qip`` to be imported as ``qutip.qip``, `#1920 `) +- Florian Hopfmueller (added a vastly improved implementations of ``process_fidelity`` and ``average_gate_fidelity``, `#1712 `, `#1748 `, `#1788 `) +- Felipe Bivort Haiek (fixed inaccuracy in docstring of the dense implementation of negation, `#1608 `) +- Rajath Shetty (added support for specifying colors for individual points, vectors and states display by `qutip.Bloch`, `#1335 `) +- Rochisha Agarwal (Add dtype to printed ouput of qobj, `#2352 `) +- Kosuke Mizuno (Add arguments of plot_wigner() and plot_wigner_fock_distribution() to specify parameters for wigner(), `#2057 `) +- Matt Ord (Only pre-compute density matrices if keep_runs_results is False, `#2303 `) +- Daniel Moreno Galán (Add the possibility to customize point colors as in V4 and fix point plot behavior for 'l' style, `#2303 `) +- Sola85 (Fixed simdiag not returning orthonormal eigenvectors, `#2269 `) +- Edward Thomas (Fix LaTeX display of Qobj state in Jupyter cell outputs, `#2272 `) +- Bogdan Reznychenko (Rework `kraus_to_choi` making it faster, `#2284 `) +- gabbence95 (Fix typos in `expect` documentation, `#2331 `) +- lklivingstone (Added __repr__ to QobjEvo, `#2111 `) +- Yuji Tamakoshi (Improve print(qutip.settings) by make it shorter, `#2113 `) +- khnikhil (Added fermionic annihilation and creation operators, `#2166 `) +- Daniel Weiss (Improved sampling algorithm for mcsolve, `#2218 `) +- SJUW (Increase missing colorbar padding for matrix_histogram_complex() from 0 to 0.05, `#2181 `) +- Valan Baptist Mathuranayagam (Changed qutip-notebooks to qutip-tutorials and fixed the typo in the link redirecting to the changelog section in the PR template, `#2107 `) +- Gerardo Jose Suarez (Added information on sec_cutoff to the documentation, `#2136 `) +- Cristian Emiliano Godinez Ramirez (Added inherited members to API doc of MESolver, SMESolver, SSESolver, NonMarkovianMCSolver, `#2167 `) +- Andrey Rakhubovsky (Corrected grammar in Bloch-Redfield master equation documentation, `#2174 `) +- Rushiraj Gadhvi (qutip.ipynbtools.version_table() can now be called without Cython installed, `#2110 `) +- Harsh Khilawala (Moved HTMLProgressBar from qutip/ipynbtools.py to qutip/ui/progressbar.py, `#2112 `) +- Avatar Srinidhi P V (Added new argument bc_type to take boundary conditions when creating QobjEvo, `#2114 `) +- NAME (DESCRIPTION, `#0000 `) + + +Qobj changes +------------ + +Previously ``Qobj`` data was stored in a SciPy-like sparse matrix. Now the +representation is flexible. Implementations for dense and sparse formats are +included in QuTiP and custom implementations are possible. QuTiP's performance +on dense states and operators is significantly improved as a result. + +Some highlights: + +- The data is still acessible via the ``.data`` attribute, but is now an + instance of the underlying data type instead of a SciPy-like sparse matrix. + The operations available in ``qutip.core.data`` may be used on ``.data``, + regardless of the data type. +- ``Qobj`` with different data types may be mixed in arithmetic and other + operations. A sensible output type will be automatically determined. +- The new ``.to(...)`` method may be used to convert a ``Qobj`` from one data type + to another. E.g. ``.to("dense")`` will convert to the dense representation and + ``.to("csr")`` will convert to the sparse type. +- Many ``Qobj`` methods and methods that create ``Qobj`` now accepted a ``dtype`` + parameter that allows the data type of the returned ``Qobj`` to specified. +- The new ``&`` operator may be used to obtain the tensor product. +- The new ``@`` operator may be used to obtain the matrix / operator product. + ``bar @ ket`` returns a scalar. +- The new ``.contract()`` method will collapse 1D subspaces of the dimensions of + the ``Qobj``. +- The new ``.logm()`` method returns the matrix logarithm of an operator. +- The methods ``.set_data``, ``.get_data``, ``.extract_state``, ``.eliminate_states``, + ``.evaluate`` and ``.check_isunitary`` have been removed. +- The property ``dtype`` return the representation of the data used. +- The new ``data_as`` allow to obtain the data as a common python formats: + numpy array, scipy sparse matrix, JAX Array, etc. + +QobjEvo changes +--------------- + +The ``QobjEvo`` type for storing time-dependent quantum objects has been +significantly expanded, standardized and extended. The time-dependent +coefficients are now represented using a new ``Coefficient`` type that +may be independently created and manipulated if required. + +Some highlights: + +- The ``.compile()`` method has been removed. Coefficients specified as + strings are automatically compiled if possible and the compilation is + cached across different Python runs and instances. +- Mixing coefficient types within a single ``Qobj`` is now supported. +- Many new attributes were added to ``QobjEvo`` for convenience. Examples + include ``.dims``, ``.shape``, ``.superrep`` and ``.isconstant``. +- Many old attributes such as ``.cte``, ``.use_cython``, ``.type``, ``.const``, + and ``.coeff_file`` were removed. +- A new ``Spline`` coefficient supports spline interpolations of different + orders. The old ``Cubic_Spline`` coefficient has been removed. +- The new ``.arguments(...)`` method allows additional arguments to the + underlying coefficient functions to be updated. +- The ``_step_func_coeff`` argument has been replaced by the ``order`` + parameter. ``_step_func_coeff=False`` is equivalent to ``order=3``. + ``_step_func_coeff=True`` is equivalent to ``order=0``. Higher values + of ``order`` gives spline interpolations of higher orders. +- The spline type can take ``bc_type`` to control the boundary conditions. +- QobjEvo can be creating from the multiplication of a Qobj with a coefficient: + ``oper * qutip.coefficient(f, args=args)`` is equivalent to + ``qutip.QobjEvo([[oper, f]], args=args)``. +- Coefficient function can be defined in a pythonic manner: ``def f(t, A, w)``. + The dictionary ``args`` second argument is no longer needed. + Function using the exact ``f(t, args)`` signature will use the old method for + backward compatibility. + +Solver changes +-------------- + +The solvers in QuTiP have been heavily reworked and standardized. +Under the hood solvers now make use of swappable ODE ``Integrators``. +Many ``Integrators`` are included (see the list below) and +custom implementations are possible. Solvers now consistently +accept a ``QobjEvo`` instance at the Hamiltonian or Liouvillian, or +any object which can be passed to the ``QobjEvo`` constructor. + +A breakdown of highlights follows. + +All solvers: + +- Solver options are now supplied in an ordinary Python dict. + ``qutip.Options`` is deprecated and returns a dict for backwards + compatibility. +- A specific ODE integrator may be selected by supplying a + ``method`` option. +- Each solver provides a class interface. Creating an instance + of the class allows a solver to be run multiple times for the + same system without having to repeatedly reconstruct the + right-hand side of the ODE to be integrated. +- A ``QobjEvo`` instance is accepted for most operators, e.g., + ``H``, ``c_ops``, ``e_ops``, ``a_ops``. +- The progress bar is now selected using the ``progress_bar`` option. + A new progess bar using the ``tqdm`` Python library is provided. +- Dynamic arguments, where the value of an operator depends on + the current state of the evolution interface reworked. Now a property of the + solver is to be used as an arguments: + ``args={"state": MESolver.StateFeedback(default=rho0)}`` + +Integrators: + +- The SciPy zvode integrator is available with the BDF and + Adams methods as ``bdf`` and ``adams``. +- The SciPy dop853 integrator (an eighth order Runge-Kutta method by + Dormand & Prince) is available as ``dop853``. +- The SciPy lsoda integrator is available as ``lsoda``. +- QuTiP's own implementation of Verner's "most efficient" Runge-Kutta methods + of order 7 and 9 are available as ``vern7`` and ``vern9``. See + http://people.math.sfu.ca/~jverner/ for a description of the methods. +- QuTiP's own implementation of a solver that directly diagonalizes the + the system to be integrated is available as ``diag``. It only works on + time-independent systems and is slow to setup, but once the diagonalization + is complete, it generates solutions very quickly. +- QuTiP's own implementatoin of an approximate Krylov subspace integrator is + available as ``krylov``. This integrator is only usable with ``sesolve``. + +Result class: + +- A new ``.e_data`` attribute provides expectation values as a dictionary. + Unlike ``.expect``, the values are provided in a Python list rather than + a numpy array, which better supports non-numeric types. +- The contents of the ``.stats`` attribute changed significantly and is + now more consistent across solvers. + +Monte-Carlo Solver (mcsolve): + +- The system, H, may now be a super-operator. +- The ``seed`` parameter now supports supplying numpy ``SeedSequence`` or + ``Generator`` types. +- The new ``timeout`` and ``target_tol`` parameters allow the solver to exit + early if a timeout or target tolerance is reached. +- The ntraj option no longer supports a list of numbers of trajectories. + Instead, just run the solver multiple times and use the class ``MCSolver`` + if setting up the solver uses a significant amount of time. +- The ``map_func`` parameter has been replaced by the ``map`` option. +- A loky based parallel map as been added. +- A mpi based parallel map as been added. +- The result returned by ``mcsolve`` now supports calculating photocurrents + and calculating the steady state over N trajectories. +- The old ``parfor`` parallel execution function has been removed from + ``qutip.parallel``. Use ``parallel_map``, ``loky_map`` or ``mpi_pmap`` instead. +- Added improved sampling options which converge much faster when the + probability of collapse is small. + +Non Markovian Monte-Carlo Solver (nm_mcsolve): + +- New Monte-Carlo Solver supporting negative decay rates. +- Enjoy of most of the improvement made to the original Monte-Carlo Solver. +- ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????/ +- Paul, anything to add? +- ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????/ + +Stochastic Equation Solvers (ssesolve, smesolve) + +- Function call greatly changed: many keyword arguments are now options. +- m_ops and dW_factors are now changed from the default from the new class interface only. +- Use the same parallel maps as mcsolve: support for loky and mpi map added. +- End conditions ``timeout`` and ``target_tol`` added. +- The ``seed`` parameter now supports supplying numpy ``SeedSequence``. +- Wiener function is now available as a feedback. + +Bloch-Redfield Master Equation Solver (brmesolve): + +- The ``a_ops`` and ``spectra`` support implementations been heavily reworked to + reuse the techniques from the new Coefficient and QobjEvo classes. +- The ``use_secular`` parameter has been removed. Use ``sec_cutoff=-1`` instead. +- The required tolerance is now read from ``qutip.settings``. + +Krylov Subspace Solver (krylovsolve): + +- The Krylov solver is now implemented using ``SESolver`` and the ``krylov`` + ODE integrator. The function ``krylovsolve`` is maintained for convenience + and now supports many more options. +- The ``sparse`` parameter has been removed. Supply a sparse ``Qobj`` for the + Hamiltonian instead. + +Floquet Solver (fsesolve and fmmesolve): + +- The Floquet solver has been rewritten to use a new ``FloquetBasis`` class + which manages the transformations from lab to Floquet basis and back. +- Many of the internal methods used by the old Floquet solvers have + been removed. The Floquet tensor may still be retried using + the function ``floquet_tensor``. +- The Floquet Markov Master Equation solver has had many changes and + new options added. The environment temperature may be specified using + ``w_th``, and the result states are stored in the lab basis and optionally + in the Floquet basis using ``store_floquet_state``. +- The spectra functions supplied to ``fmmesolve`` must now be vectorized + (i.e. accept and return numpy arrays for frequencies and densities) and + must accept negative frequence (i.e. usually include a ``w > 0`` factor + so that the returned densities are zero for negative frequencies). +- The number of sidebands to keep, ``kmax`` may only be supplied when using + the ``FMESolver`` +- The ``Tsteps`` parameter has been removed from both ``fsesolve`` and + ``fmmesolve``. The ``precompute`` option to ``FloquetBasis`` may be used + instead. + +Evolution of State Solver (essovle): + +- The function ``essolve`` has been removed. Use the ``diag`` integration + method with ``sesolve`` or ``mesolve`` instead. + +Steady-state solvers (steadystate module): + +- The ``method`` parameter and ``solver`` parameters have been separated. Previously + they were mixed together in the ``method`` parameter. +- The previous options are now passed as parameters to the steady state + solver and mostly passed through to the underlying SciPy functions. +- The logging and statistics have been removed. + +Correlation functions (correlation module): + +- A new ``correlation_3op`` function has been added. It supports ``MESolver`` + or ``BRMESolver``. +- The ``correlation``, ``correlation_4op``, and ``correlation_ss`` functions have been + removed. +- Support for calculating correlation with ``mcsolve`` has been removed. + +Propagators (propagator module): + +- A class interface, ``qutip.Propagator``, has been added for propagators. +- Propagation of time-dependent systems is now supported using ``QobjEvo``. +- The ``unitary_mode`` and ``parallel`` options have been removed. + +Correlation spectra (spectrum module): + +- The functions ``spectrum_ss`` and ``spectrum_pi`` have been removed and + are now internal functions. +- The ``use_pinv`` parameter for ``spectrum`` has been removed and the + functionality merged into the ``solver`` parameter. Use ``solver="pi"`` + instead. + +Hierarchical Equation of Motion Solver (HEOM) + +- Added support for combining bosonic and fermionic baths. +- ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? +- Simon, there is a lot more than you and other have done for HEOM, please brag a little +- ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? + + +QuTiP core +---------- + +There have been numerous other small changes to core QuTiP features: + +- ``qft(...)`` the function that returns the quantum Fourier + transform operator was moved from ``qutip.qip.algorithm`` into ``qutip``. +- The Bloch-Redfield solver tensor, ``brtensor``, has been moved into + ``qutip.core``. See the section above on the Bloch-Redfield solver + for details. +- The functions ``mat2vec`` and ``vec2mat`` for transforming states to and + from super-operator states have been renamed to ``stack_columns`` and + ``unstack_columns``. +- The function ``liouvillian_ref`` has been removed. Used ``liouvillian`` + instead. +- The superoperator transforms ``super_to_choi``, ``choi_to_super``, + ``choi_to_kraus``, ``choi_to_chi`` and ``chi_to_choi`` have been removed. + Used ``to_choi``, ``to_super``, ``to_kraus`` and ``to_chi`` instead. +- All of the random object creation functions now accepted a + numpy ``Generator`` as a seed. +- The ``dims`` parameter of all random object creation functions has + been removed. Supply the dimensions as the first parameter if + explicit dimensions are required. +- The function ``rand_unitary_haar`` has been removed. Use + ``rand_unitary(distribution="haar")`` instead. +- The functions ``rand_dm_hs`` and ``rand_dm_ginibre`` have been removed. + Use ``rand_dm(distribution="hs")`` and ``rand_dm(distribution="ginibre")`` + instead. +- The function ``rand_ket_haar`` has been removed. Use + ``rand_ket(distribution="haar")`` instead. +- The measurement functions have had the ``target`` parameter for + expanding the measurement operator removed. Used ``expand_operator`` + to expand the operator instead. +- ``qutip.Bloch`` now supports applying colours per-point, state or vector in + ``add_point``, ``add_states``, and ``add_vectors``. +- Dimensions use a class instead of layered lists. +- Allow measurement functions to support degenerate operators. +- Add ``qeye_like`` and ``qzero_like``. +- Added fermionic annihilation and creation operators. + +QuTiP settings +-------------- + +Previously ``qutip.settings`` was an ordinary module. Now ``qutip.settings`` is +an instance of a settings class. All the runtime modifiable settings for +core operations are in ``qutip.settings.core``. The other settings are not +modifiable at runtime. + +- Removed ``load``. ``reset`` and ``save`` functions. +- Removed ``.debug``, ``.fortran``, ``.openmp_thresh``. +- New ``.compile`` stores the compilation options for compiled coefficients. +- New ``.core["rtol"]`` core option gives the default relative tolerance used by QuTiP. +- The absolute tolerance setting ``.atol`` has been moved to ``.core["atol"]``. + +Visualization +------------- + +- Add arguments of plot_wigner() and plot_wigner_fock_distribution() to specify parameters for wigner(). +- Removed Bloch3D: redundant to Bloch. +- Improved the function calls, all visualization functions take ``fig``, ``ax`` and ``cmap`` keyword arguments. +- Most visualization functions respect the ``colorblind_safe`` setting. +- Added new functions to create animations from a list of Qobj or directly from solver results (if states are saved.) +- + + +Package reorganization +---------------------- + +- ``qutip.qip`` has been moved into its own package, qutip-qip. Once installed, qutip-qip is available as either ``qutip.qip`` or ``qutip_qip``. Some widely useful gates have been retained in ``qutip.gates``. +- ``qutip.lattice`` has been moved into its own package, qutip-lattice. It is available from ``. +- ``qutip.sparse`` has been removed. It contained the old sparse matrix representation and is replaced by the new implementation in ``qutip.data``. +- ``qutip.piqs`` functions are no longer available from the ``qutip`` namespace. They are accessible from ``qutip.piqs`` instead. + +Miscellaneous +------------- + +- Support has been added for 64-bit integer sparse matrix indices, allowing + sparse matrices with up to 2**63 rows and columns. This support needs to + be enabled at compilation time by calling ``setup.py`` and passing + ``--with-idxint-64``. + +Feature removals +---------------- + +- Support for OpenMP has been removed. If there is enough demand and a good plan for how to organize it, OpenMP support may return in a future QuTiP release. +- The ``qutip.parfor`` function has been removed. Use ``qutip.parallel_map`` instead. +- ``qutip.graph`` has been removed and replaced by SciPy's graph functions. +- ``qutip.topology`` has been removed. It contained only one function ``berry_curvature``. +- The ``~/.qutip/qutiprc`` config file is no longer supported. It contained settings for the OpenMP support. + + +Changes from QuTiP 5.0.0b1: +-------------------------- + +Features +-------- + +- Add dtype to printed ouput of qobj (#2352 by Rochisha Agarwal) + + +Miscellaneous +------------- + +- Allow scipy 1.12 to be used with qutip. (#2354 by Pieter Eendebak) + + QuTiP 5.0.0b1 (2024-03-04) ========================== @@ -58,7 +513,7 @@ Features - Change the order of parameters in expand_operator (#1991) - Add `svn` and `solve` to dispatched (#2002) - Added nm_mcsolve to provide support for Monte-Carlo simulations of master equations with possibly negative rates. The method implemented here is described in arXiv:2209.08958 [quant-ph]. (#2070 by pmenczel) -- Add support for combining bosinic and fermionic HEOM baths (#2089) +- Add support for combining bosonic and fermionic HEOM baths (#2089) - Added __repr__ to QobjEvo (#2111 by lklivingstone) - Improve print(qutip.settings) by make it shorter (#2113 by tamakoshi2001) - Create the `trace_oper_ket` operation (#2126) diff --git a/doc/changes/2352.feature b/doc/changes/2352.feature deleted file mode 100644 index 96b9a87c15..0000000000 --- a/doc/changes/2352.feature +++ /dev/null @@ -1 +0,0 @@ -Add dtype to printed ouput of qobj \ No newline at end of file diff --git a/doc/changes/2354.misc b/doc/changes/2354.misc deleted file mode 100644 index e3cfa9de56..0000000000 --- a/doc/changes/2354.misc +++ /dev/null @@ -1 +0,0 @@ -Allow scipy 1.12 to be used with qutip. \ No newline at end of file From 9c61cd55415f9f24bd7123f4487d6e5321bc7b0f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 27 Mar 2024 07:38:25 +0900 Subject: [PATCH 054/305] Add last minute PR --- doc/changelog.rst | 2 +- doc/changes/2363.doc | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 doc/changes/2363.doc diff --git a/doc/changelog.rst b/doc/changelog.rst index 18f0a93a33..41e916a3d0 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -111,7 +111,7 @@ detailed below: - Rushiraj Gadhvi (qutip.ipynbtools.version_table() can now be called without Cython installed, `#2110 `) - Harsh Khilawala (Moved HTMLProgressBar from qutip/ipynbtools.py to qutip/ui/progressbar.py, `#2112 `) - Avatar Srinidhi P V (Added new argument bc_type to take boundary conditions when creating QobjEvo, `#2114 `) -- NAME (DESCRIPTION, `#0000 `) +- Andrey Rakhubovsky (Fix types in docstring of projection(), `#2363 `) Qobj changes diff --git a/doc/changes/2363.doc b/doc/changes/2363.doc deleted file mode 100644 index 981f95c320..0000000000 --- a/doc/changes/2363.doc +++ /dev/null @@ -1 +0,0 @@ -Fix types in docstring of projection() From 020162ed9f2782600cc2b926aecb648b7603ba56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Tue, 26 Mar 2024 21:31:25 -0400 Subject: [PATCH 055/305] Update doc/changelog.rst Co-authored-by: Paul --- doc/changelog.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 41e916a3d0..eaa2c9896b 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -262,10 +262,9 @@ Monte-Carlo Solver (mcsolve): Non Markovian Monte-Carlo Solver (nm_mcsolve): - New Monte-Carlo Solver supporting negative decay rates. -- Enjoy of most of the improvement made to the original Monte-Carlo Solver. -- ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????/ -- Paul, anything to add? -- ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????/ +- Based on the influence martingale approach, Donvil et al., Nat Commun 13, 4140 (2022). +- Most of the improvements made to the regular Monte-Carlo solver are also available here. +- The value of the influence martingale is available through the ``.trace`` attribute of the result. Stochastic Equation Solvers (ssesolve, smesolve) From 0eb1861aafa799f27f7603f697924a2e19d2859a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Tue, 26 Mar 2024 22:25:22 -0400 Subject: [PATCH 056/305] Apply suggestions from code review Co-authored-by: Simon Cross --- doc/changelog.rst | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index eaa2c9896b..ea1488a993 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -29,7 +29,7 @@ Much of the user-facing API of QuTiP remains familiar, but there have had to be many small breaking changes. If we can make changes to easy migrating code from QuTiP 4 to QuTiP 5, please let us know. -Any extensive list of changes follows. +An extensive list of changes follows. Contributors ------------ @@ -59,13 +59,13 @@ QuTiP 5: - Christian Staufenbiel updated many of the tutorials (``). - Xavier Sproken update the benchmarks (``). -During an internship at RIKEN, Patrick Hopf created new quantum control method and +During an internship at RIKEN, Patrick Hopf created a new quantum control method and improved the existing methods interface: - Patrick Hopf created new quantum control package (``). Four experimental data layers backends were written either as part of Google Summer -of Code or as separate projects. While these are still alpha quality, the helped +of Code or as separate projects. While these are still alpha quality, they helped significantly to test the data layer API: - ``qutip-tensorflow``: a TensorFlow backend by Asier Galicia (``) @@ -348,10 +348,11 @@ Correlation spectra (spectrum module): Hierarchical Equation of Motion Solver (HEOM) +- Updated the solver to use the new QuTiP integrators and data layer. +- Updated all the HEOM tutorials to QuTiP 5. - Added support for combining bosonic and fermionic baths. -- ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? -- Simon, there is a lot more than you and other have done for HEOM, please brag a little -- ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? +- Sped up the construction of the RHS of the HEOM solver by a factor of 4x. +- As in QuTiP 4, the HEOM supports arbitrary spectral densities, bosonic and fermionic baths, Páde and Matsubara expansions of the correlation functions, calculating the Matsubara terminator and inspection of the ADOs (auxiliary density operators). QuTiP core @@ -411,18 +412,18 @@ modifiable at runtime. Visualization ------------- -- Add arguments of plot_wigner() and plot_wigner_fock_distribution() to specify parameters for wigner(). -- Removed Bloch3D: redundant to Bloch. -- Improved the function calls, all visualization functions take ``fig``, ``ax`` and ``cmap`` keyword arguments. -- Most visualization functions respect the ``colorblind_safe`` setting. -- Added new functions to create animations from a list of Qobj or directly from solver results (if states are saved.) -- +- Added arguments to ``plot_wigner`` and ``plot_wigner_fock_distribution`` to specify parameters for ``wigner``. +- Removed ``Bloch3D``. The same functionality is provided by ``Bloch``. +- Added ``fig``, ``ax`` and ``cmap`` keyword arguments to all visualization functions. +- Most visualization functions now respect the ``colorblind_safe`` setting. +- Added new functions to create animations from a list of ``Qobj`` or directly from solver results with saved states. Package reorganization ---------------------- - ``qutip.qip`` has been moved into its own package, qutip-qip. Once installed, qutip-qip is available as either ``qutip.qip`` or ``qutip_qip``. Some widely useful gates have been retained in ``qutip.gates``. +- ``qutip.control`` has been moved to qutip-qtrl and once installed qutip-qtrl is available as either ``qutip.control`` or ``qutip_qtrl``. Note that ``quitp_qtrl`` is provided primarily for backwards compatibility. Improvements to optimal control will take place in the new ``qutip_qoc`` package. - ``qutip.lattice`` has been moved into its own package, qutip-lattice. It is available from ``. - ``qutip.sparse`` has been removed. It contained the old sparse matrix representation and is replaced by the new implementation in ``qutip.data``. - ``qutip.piqs`` functions are no longer available from the ``qutip`` namespace. They are accessible from ``qutip.piqs`` instead. From f05f4a247abf8bd52a3e114a9ec315dbc94573c7 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 27 Mar 2024 11:27:43 +0900 Subject: [PATCH 057/305] Add removal entries --- doc/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index ea1488a993..c4125aeb41 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -444,6 +444,8 @@ Feature removals - ``qutip.graph`` has been removed and replaced by SciPy's graph functions. - ``qutip.topology`` has been removed. It contained only one function ``berry_curvature``. - The ``~/.qutip/qutiprc`` config file is no longer supported. It contained settings for the OpenMP support. +- Deprecate ``three_level_atom`` +- Deprecate ``orbital`` Changes from QuTiP 5.0.0b1: From 29635fbb1fea3acf5982ed635dcd8284a5025230 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 27 Mar 2024 13:05:16 +0900 Subject: [PATCH 058/305] Fix changelog issues --- doc/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index c4125aeb41..c515aaa5d1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -212,7 +212,7 @@ All solvers: - Dynamic arguments, where the value of an operator depends on the current state of the evolution interface reworked. Now a property of the solver is to be used as an arguments: - ``args={"state": MESolver.StateFeedback(default=rho0)}`` + ``args={"state": MESolver.StateFeedback(default=rho0)}`` Integrators: @@ -449,7 +449,7 @@ Feature removals Changes from QuTiP 5.0.0b1: --------------------------- +--------------------------- Features -------- From b85af2e5ddc6ca196732e70fccba0118cb2dcd2f Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 28 Mar 2024 19:15:31 +0900 Subject: [PATCH 059/305] Weighted trajectories --- qutip/solver/mcsolve.py | 6 +- qutip/solver/multitraj.py | 9 +- qutip/solver/nm_mcsolve.py | 4 +- qutip/solver/result.py | 614 +++++++++----------------- qutip/solver/stochastic.py | 5 +- qutip/tests/solver/test_nm_mcsolve.py | 4 +- qutip/tests/solver/test_results.py | 6 +- 7 files changed, 239 insertions(+), 409 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index d35aa972a1..1e0e94e230 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -496,9 +496,9 @@ def run(self, state, tlist, ntraj=1, *, e_ops=e_ops, timeout=timeout, target_tol=target_tol, seeds=seeds) - stats, seeds, result, map_func, map_kw, state0 = self._initialize_run( - state, ntraj, args=args, timeout=timeout, - target_tol=target_tol, seeds=seeds, + seeds, result, map_func, map_kw, state0 = self._initialize_run( + state, ntraj, args=args, e_ops=e_ops, + timeout=timeout, target_tol=target_tol, seeds=seeds ) # first run the no-jump trajectory diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index c040a4e7d0..78c60e31e0 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -129,7 +129,7 @@ def step(self, t, *, args=None, copy=True): _, state = self._integrator.integrate(t, copy=False) return self._restore_state(state, copy=copy) - def _initialize_run(self, state, ntraj=1, args=None, + def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): start_time = time() self._argument(args) @@ -137,7 +137,7 @@ def _initialize_run(self, state, ntraj=1, args=None, seeds = self._read_seed(seeds, ntraj) result = self._resultclass( - self.options, solver=self.name, stats=stats + e_ops, self.options, solver=self.name, stats=stats ) result.add_end_condition(ntraj, target_tol) @@ -148,7 +148,7 @@ def _initialize_run(self, state, ntraj=1, args=None, }) state0 = self._prepare_state(state) stats['preparation time'] += time() - start_time - return stats, seeds, result, map_func, map_kw, state0 + return seeds, result, map_func, map_kw, state0 def run(self, state, tlist, ntraj=1, *, args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): @@ -211,10 +211,11 @@ def run(self, state, tlist, ntraj=1, *, The simulation will end when the first end condition is reached between ``ntraj``, ``timeout`` and ``target_tol``. """ - stats, seeds, result, map_func, map_kw, state0 = self._initialize_run( + seeds, result, map_func, map_kw, state0 = self._initialize_run( state, ntraj, args=args, + e_ops=e_ops, timeout=timeout, target_tol=target_tol, seeds=seeds, diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 26d4118265..a6c8473b77 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -5,6 +5,7 @@ import numpy as np import scipy +from .result import NmmcResult from .multitraj import MultiTrajSolver from .mcsolve import MCSolver, MCIntegrator from .mesolve import MESolver, mesolve @@ -333,6 +334,7 @@ class NonMarkovianMCSolver(MCSolver): Options for the evolution. """ name = "nm_mcsolve" + _resultclass = NmmcResult solver_options = { "progress_bar": "text", "progress_kwargs": {"chunk_size": 10}, @@ -388,7 +390,6 @@ def __init__( ] super().__init__(H, c_ops, options=options) - @property def _mc_integrator_class(self, *args): return NmMCIntegrator(*args, __martingale=self._martingale) @@ -535,6 +536,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): **integrator_kwargs) martingales = [self._martingale.value(t) for t in tlist] result.add_relative_weight(martingales) + result.trace = martingales return seed, result def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 244b4b1ff9..cfa3be64e0 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1,8 +1,9 @@ """ Class for solve function results""" +from copy import copy from typing import TypedDict import numpy as np -from ..core import Qobj, QobjEvo, expect, ket2dm, qzero_like +from ..core import Qobj, QobjEvo, expect, qzero_like __all__ = [ "Result", @@ -491,10 +492,12 @@ class MultiTrajResult(_BaseResult): options: MultiTrajResultOptions def __init__( - self, options: MultiTrajResultOptions, *, + self, e_ops, options: MultiTrajResultOptions, *, solver=None, stats=None, **kw, ): super().__init__(options, solver=solver, stats=stats) + self.e_ops = e_ops or [] + self._raw_ops = self._e_ops_to_dict(e_ops) self.trajectories = [] self.num_trajectories = 0 @@ -502,26 +505,16 @@ def __init__( self.average_e_data = {} self.std_e_data = {} - self.runs_e_data = None + self.runs_e_data = {} - # Will be initialized at the first trajectory: - self.e_ops = None + # Will be initialized at the first trajectory self.times = None # We separate all sums into terms of trajectories with specified - # absolute weight (`_abs`) or without (`_rel`). The `_rel` will be - # initialized at the first trajectory, the `_abs` when needed - self._sum_states_abs = None - self._sum_states_rel = None - self._sum_final_states_abs = None - self._sum_final_states_rel = None - self._sum_expect_abs = None - self._sum_expect_rel = None - self._sum2_expect_abs = None - self._sum2_expect_rel = None - - # Whether we have seen any trajectories with specified absolute weight - self._abs_weights_present = False + # absolute weight (`_abs`) or without (`_rel`). They will be initialized + # when the first trajectory of the respective type is added. + self._sum_rel = None + self._sum_abs = None # Number of trajectories without specified absolute weight self._num_rel_trajectories = 0 @@ -530,8 +523,8 @@ def __init__( @property def _store_average_density_matrices(self) -> bool: return ( - self.options["store_states"] - or (self.options["store_states"] is None and self.e_ops == {}) + self.options["store_states"] or + (self.options["store_states"] is None and len(self._raw_ops) == 0) ) and not self.options["keep_runs_results"] @property @@ -547,114 +540,79 @@ def _add_first_traj(self, trajectory): Read the first trajectory, intitializing needed data. """ self.times = trajectory.times - self.e_ops = trajectory.e_ops - - if trajectory.states and self._store_average_density_matrices: - self._sum_states_rel = [ - qzero_like(ket2dm(state)) for state in trajectory.states - ] - - if trajectory.final_state and self._store_final_density_matrix: - state = trajectory.final_state - self._sum_final_states_rel = qzero_like(ket2dm(state)) - - self._sum_expect_rel = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - self._sum2_expect_rel = [ - np.zeros_like(expect) for expect in trajectory.expect - ] if self.options["keep_runs_results"]: - self.runs_e_data = {k: [] for k in self.e_ops} - - def _first_abs_trajectory(self): - if self._sum_states_rel: - self._sum_states_abs = [ - qzero_like(state) for state in self._sum_states_rel - ] - if self._sum_final_states_rel: - self._sum_final_states_abs = qzero_like(self._sum_final_states_rel) - self._sum_expect_abs = [ - np.zeros_like(expect) for expect in self._sum_expect_rel - ] - self._sum2_expect_abs = [ - np.zeros_like(expect) for expect in self._sum2_expect_rel - ] - self._abs_weights_present = True + self.runs_e_data = {k: [] for k in self._raw_ops} def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) def _reduce_states(self, trajectory): if trajectory.has_absolute_weight(): - self._sum_states_abs = [ - accu + weight * ket2dm(state) - for accu, state, weight in zip(self._sum_states_abs, - trajectory.states, - trajectory._weight_array()) - ] - elif trajectory.has_weight(): - self._sum_states_rel = [ - accu + weight * ket2dm(state) - for accu, state, weight in zip(self._sum_states_rel, - trajectory.states, - trajectory._weight_array()) - ] + self._sum_abs.reduce_states(trajectory) else: - self._sum_states_rel = [ - accu + ket2dm(state) - for accu, state in zip(self._sum_states_rel, trajectory.states) - ] + self._sum_rel.reduce_states(trajectory) def _reduce_final_state(self, trajectory): if trajectory.has_absolute_weight(): - self._sum_final_states_abs += (trajectory._final_weight() * - ket2dm(trajectory.final_state)) - elif trajectory.has_weight(): - self._sum_final_states_rel += (trajectory._final_weight() * - ket2dm(trajectory.final_state)) + self._sum_abs.reduce_final_state(trajectory) else: - self._sum_final_states_rel += ket2dm(trajectory.final_state) + self._sum_rel.reduce_final_state(trajectory) def _reduce_expect(self, trajectory): """ Compute the average of the expectation values and store it in it's multiple formats. """ - weight = trajectory.rel_weight if trajectory.has_absolute_weight(): - weight = weight * trajectory.abs_weight + self._sum_abs.reduce_expect(trajectory) + else: + self._sum_rel.reduce_expect(trajectory) - for i, k in enumerate(self.e_ops): - expect_traj = trajectory.expect[i] + self._create_e_data() - if trajectory.has_absolute_weight(): - self._sum_expect_abs[i] += weight * expect_traj - self._sum2_expect_abs[i] += weight * expect_traj**2 - else: - self._sum_expect_rel[i] += weight * expect_traj - self._sum2_expect_rel[i] += weight * expect_traj**2 + if self.runs_e_data: + for k in self._raw_ops: + self.runs_e_data[k].append(trajectory.e_data[k]) - avg = (self._sum_expect_abs[i] + - self._sum_expect_rel[i] / self._num_rel_trajectories) - avg2 = (self._sum2_expect_abs[i] + - self._sum2_expect_rel[i] / self._num_rel_trajectories) + def _create_e_data(self): + for i, k in enumerate(self._raw_ops): + avg = 0 + avg2 = 0 + if self._sum_abs: + avg += self._sum_abs.sum_expect[i] + avg2 += self._sum_abs.sum2_expect[i] + if self._sum_rel: + avg += ( + self._sum_rel.sum_expect[i] / self._num_rel_trajectories + ) + avg2 += ( + self._sum_rel.sum2_expect[i] / self._num_rel_trajectories + ) self.average_e_data[k] = list(avg) - # mean(expect**2) - mean(expect)**2 can something be very small # negative (-1e-15) which raise an error for float sqrt. self.std_e_data[k] = list(np.sqrt(np.abs(avg2 - np.abs(avg**2)))) - if self.runs_e_data: - self.runs_e_data[k].append(trajectory.e_data[k]) - def _increment_traj(self, trajectory): if self.num_trajectories == 0: self._add_first_traj(trajectory) - if trajectory.has_absolute_weight() and not self._abs_weights_present: - self._first_abs_trajectory() + + if trajectory.has_absolute_weight(): + if self._sum_abs is None: + self._sum_abs = _TrajectorySum( + trajectory, + self._store_average_density_matrices, + self._store_final_density_matrix) + else: + self._num_rel_trajectories += 1 + if self._sum_rel is None: + self._sum_rel = _TrajectorySum( + trajectory, + self._store_average_density_matrices, + self._store_final_density_matrix) + self.num_trajectories += 1 def _no_end(self): @@ -674,8 +632,8 @@ def _fixed_end(self): return ntraj_left def _average_computer(self): - avg = np.array(self._sum_expect_rel) / self._num_rel_trajectories - avg2 = np.array(self._sum2_expect_rel) / self._num_rel_trajectories + avg = np.array(self._sum_rel.sum_expect) / self._num_rel_trajectories + avg2 = np.array(self._sum_rel.sum2_expect) / self._num_rel_trajectories return avg, avg2 def _target_tolerance_end(self): @@ -816,27 +774,33 @@ def average_states(self): """ States averages as density matrices. """ - if self._sum_states_rel is None: - if not (self.trajectories and self.trajectories[0].states): + + trajectory_states_available = (self.trajectories and + self.trajectories[0].states) + need_to_reduce_states = False + if self._sum_abs and not self._sum_abs.sum_states: + if not trajectory_states_available: return None - self._sum_states_rel = [ - qzero_like(ket2dm(state)) - for state in self.trajectories[0].states - ] - if self._abs_weights_present: - self._sum_states_abs = [ - qzero_like(ket2dm(state)) - for state in self.trajectories[0].states - ] + self._sum_abs._initialize_sum_states(self.trajectories[0]) + need_to_reduce_states = True + if self._sum_rel and not self._sum_rel.sum_states: + if not trajectory_states_available: + return None + self._sum_rel._initialize_sum_states(self.trajectories[0]) + need_to_reduce_states = True + if need_to_reduce_states: for trajectory in self.trajectories: self._reduce_states(trajectory) - result = [sum_rel / self._num_rel_trajectories - for sum_rel in self._sum_states_rel] - if self._abs_weights_present: - result = [res + sum_abs - for res, sum_abs in zip(result, self._sum_states_abs)] - return result + if self._sum_abs and self._sum_rel: + return [a + r / self._num_rel_trajectories for a, r in zip( + self._sum_abs.sum_states, self._sum_rel.sum_states) + ] + if self._sum_rel: + return [r / self._num_rel_trajectories + for r in self._sum_rel.sum_states + ] + return self._sum_abs.sum_states @property def states(self): @@ -860,15 +824,19 @@ def average_final_state(self): """ Last states of each trajectories averaged into a density matrix. """ - if self._sum_final_states_rel is None: - if self.average_states is not None: - return self.average_states[-1] + if ((self._sum_abs and not self._sum_abs.sum_final_state) + or (self._sum_rel and not self._sum_rel.sum_final_state)): + + if (average_states := self.average_states) is not None: + return average_states[-1] return None - result = self._sum_final_states_rel / self._num_rel_trajectories - if self._abs_weights_present: - result += self._sum_final_states_abs - return result + if self._sum_rel: + result = self._sum_rel.sum_final_state / self._num_rel_trajectories + if self._sum_abs: + result += self._sum_abs.sum_final_state + return result + return self._sum_abs.sum_final_state @property def final_state(self): @@ -953,79 +921,118 @@ def __add__(self, other): raise ValueError("Shared `times` are is required to merge results") new = self.__class__( - self.options, solver=self.solver, stats=self.stats + self.e_ops, self.options, solver=self.solver, stats=self.stats ) new.times = self.times - new.e_ops = self.e_ops if self.trajectories and other.trajectories: new.trajectories = self.trajectories + other.trajectories new.num_trajectories = self.num_trajectories + other.num_trajectories - new.seeds = self.seeds + other.seeds - new._num_rel_trajectories = (self._num_rel_trajectories + other._num_rel_trajectories) - new._abs_weights_present = (self._abs_weights_present or - other._abs_weights_present) - if new._abs_weights_present: - new._first_abs_trajectory() # initialize abs fields - #TODO shit this only works after all the "_rel" are initialized... - - if ( - self._sum_states_rel is not None - and other._sum_states_rel is not None - ): - new._sum_states_rel = [ - state1 + state2 for state1, state2 in zip( - self._sum_states_rel, other._sum_states_rel - ) - ] - if self._abs_weights_present: - new._sum_states_abs = list(self._sum_states_abs) - if other._abs_weights_present: - new._sum_states_abs = [ - state1 + state2 for state1, state2 in zip( - new._sum_states_abs, other._sum_states_abs - ) - ] + new.seeds = self.seeds + other.seeds + + if self._sum_abs: + new._sum_abs = self._sum_abs + other._sum_abs + if self._sum_rel: + new._sum_rel = self._sum_rel + other._sum_rel + + new._create_e_data() - if ( - self._sum_final_states_rel is not None - and other._sum_final_states_rel is not None - ): - new._sum_final_states_rel = (self._sum_final_states_rel - + other._sum_final_states_rel) - if self._abs_weights_present: - new._sum_final_states_abs = self._sum_final_states_abs - if other._abs_weights_present: - new._sum_final_states_abs += other._sum_final_states_abs - - new._sum_expect_rel = [] - new._sum2_expect_rel = [] - if self._abs_weights_present: - new._sum_expect_abs = [] - new._sum2_expect_abs = [] if self.runs_e_data and other.runs_e_data: new.runs_e_data = {} + for k in self._raw_ops: + new.runs_e_data[k] = self.runs_e_data[k] + other.runs_e_data[k] - for i, k in enumerate(self.e_ops): - new._sum_expect_rel.append( - self._sum_expect_rel[i] + other._sum_expect_rel[i]) - new._sum2_expect_rel.append( - self._sum2_expect_rel[i] + other._sum2_expect_rel[i] - ) + new.stats["run time"] += other.stats["run time"] + new.stats["end_condition"] = "Merged results" - avg = new._sum_expect[i] / new.num_trajectories - avg2 = new._sum2_expect[i] / new.num_trajectories + return new - new.average_e_data[k] = list(avg) - new.std_e_data[k] = np.sqrt(np.abs(avg2 - np.abs(avg**2))) - if self.runs_e_data and other.runs_e_data: - new.runs_e_data[k] = self.runs_e_data[k] + other.runs_e_data[k] +class _TrajectorySum: + def __init__(self, example_trajectory, store_states, store_final_state): + if example_trajectory.states and store_states: + self._initialize_sum_states(example_trajectory) + else: + self.sum_states = None - new.stats["run time"] += other.stats["run time"] - new.stats["end_condition"] = "Merged results" + if (fstate := example_trajectory.final_state) and store_final_state: + self.sum_final_state = qzero_like(_to_dm(fstate)) + else: + self.sum_final_state = None + + self.sum_expect = [ + np.zeros_like(expect) for expect in example_trajectory.expect + ] + self.sum2_expect = [ + np.zeros_like(expect) for expect in example_trajectory.expect + ] + + def _initialize_sum_states(self, example_trajectory): + self.sum_states = [ + qzero_like(_to_dm(state)) for state in example_trajectory.states + ] + + def reduce_states(self, trajectory): + if trajectory.has_weight(): + self.sum_states = [ + accu + weight * _to_dm(state) + for accu, state, weight in zip(self.sum_states, + trajectory.states, + trajectory._total_weight()) + ] + else: + self.sum_states = [ + accu + _to_dm(state) + for accu, state in zip(self.sum_states, trajectory.states) + ] + + def reduce_final_state(self, trajectory): + if trajectory.has_weight(): + self.sum_final_state += (trajectory._final_weight() * + _to_dm(trajectory.final_state)) + else: + self.sum_final_state += _to_dm(trajectory.final_state) + + def reduce_expect(self, trajectory): + if trajectory.has_weight(): + weight = trajectory._total_weight() + else: + weight = 1 + + for i, expect_traj in enumerate(trajectory.expect): + self.sum_expect[i] += weight * expect_traj + self.sum2_expect[i] += weight * expect_traj**2 + + def __add__(self, other): + if other is None: + return self + if not isinstance(other, _TrajectorySum): + raise NotImplementedError("Can only add two trajectory sums") + + new = copy(self) + + if self.sum_states and other.sum_states: + new.sum_states = [ + state1 + state2 for state1, state2 in zip( + self.sum_states, other.sum_states + ) + ] + else: + new.sum_states = None + + if self.sum_final_state and other.sum_final_state: + new.sum_final_state += other.sum_final_state + else: + new.sum_final_state = None + + new.sum_expect = [sum1 + sum2 for sum1, sum2 in zip( + self.sum_expect, other.sum_expect) + ] + new.sum2_expect = [sum1 + sum2 for sum1, sum2 in zip( + self.sum2_expect, other.sum2_expect) + ] return new @@ -1094,7 +1101,7 @@ def has_absolute_weight(self): """Whether an absolute weight has been set.""" return (self.abs_weight is not None) - def _weight_array(self): + def _total_weight(self): """ Returns an array containing the weight as a function of time. If no absolute weight was set, this is only the relative weight. If an @@ -1107,7 +1114,8 @@ def _weight_array(self): return weights def _final_weight(self): - return self._weight_array()[-1] + return self._total_weight()[-1] + class McResult(MultiTrajResult): """ @@ -1187,6 +1195,8 @@ def photocurrent(self): """ Average photocurrent or measurement of the evolution. """ + # TODO this is wrong if trajectories have weights + # unclear how to implement in case of time-dependent weights cols = [[] for _ in range(self.num_c_ops)] tlist = self.times for collapses in self.collapse: @@ -1205,6 +1215,8 @@ def runs_photocurrent(self): """ Photocurrent or measurement of each runs. """ + # TODO this is wrong if trajectories have weights + # unclear how to implement in case of time-dependent weights tlist = self.times measurements = [] for collapses in self.collapse: @@ -1267,31 +1279,42 @@ class NmmcResult(McResult): def _post_init(self): super()._post_init() - self._sum_trace = None - self._sum2_trace = None - self.average_trace = [] - self.std_trace = [] + self._sum_trace_abs = None + self._sum_trace_rel = None + self._sum2_trace_abs = None + self._sum2_trace_rel = None + + self.average_trace = None + self.std_trace = None self.runs_trace = [] self.add_processor(self._add_trace) def _add_first_traj(self, trajectory): super()._add_first_traj(trajectory) - self._sum_trace = np.zeros_like(trajectory.times) - self._sum2_trace = np.zeros_like(trajectory.times) + self._sum_trace_abs = np.zeros_like(trajectory.times) + self._sum_trace_rel = np.zeros_like(trajectory.times) + self._sum2_trace_abs = np.zeros_like(trajectory.times) + self._sum2_trace_rel = np.zeros_like(trajectory.times) def _add_trace(self, trajectory): - new_trace = np.array(trajectory.trace) - self._sum_trace += new_trace - self._sum2_trace += np.abs(new_trace) ** 2 + if trajectory.has_absolute_weight(): + self._sum_trace_abs += trajectory._total_weight() + self._sum2_trace_abs += np.abs(trajectory._total_weight()) ** 2 + else: + self._sum_trace_rel += trajectory._total_weight() + self._sum2_trace_rel += np.abs(trajectory._total_weight()) ** 2 - avg = self._sum_trace / self.num_trajectories - avg2 = self._sum2_trace / self.num_trajectories + avg = (self._sum_trace_abs + + self._sum_trace_rel / self._num_rel_trajectories) + avg2 = (self._sum2_trace_abs + + self._sum2_trace_rel / self._num_rel_trajectories) self.average_trace = avg self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) if self.options["keep_runs_results"]: + # TODO rename this to runs_martingales? self.runs_trace.append(trajectory.trace) @property @@ -1303,206 +1326,7 @@ def trace(self): return self.runs_trace or self.average_trace - - - - - - - - - - - - - - - - - - - -class McResultImprovedSampling(McResult, MultiTrajResult): - """ - See docstring for McResult and MultiTrajResult for all relevant documentation. - This class computes expectation values and sums of states, etc - using the improved sampling algorithm, which samples the no-jump trajectory - first and then only samples jump trajectories afterwards. - """ - - def __init__(self, e_ops, options, **kw): - MultiTrajResult.__init__(self, e_ops=e_ops, options=options, **kw) - self._sum_expect_no_jump = None - self._sum_expect_jump = None - self._sum2_expect_no_jump = None - self._sum2_expect_jump = None - - self._sum_states_no_jump = None - self._sum_states_jump = None - self._sum_final_states_no_jump = None - self._sum_final_states_jump = None - - self.no_jump_prob = None - - def _reduce_states(self, trajectory): - if self.num_trajectories == 1: - self._sum_states_no_jump = [ - accu + ket2dm(state) - for accu, state in zip( - self._sum_states_no_jump, trajectory.states - ) - ] - else: - self._sum_states_jump = [ - accu + ket2dm(state) - for accu, state in zip( - self._sum_states_jump, trajectory.states - ) - ] - - def _reduce_final_state(self, trajectory): - dm_final_state = ket2dm(trajectory.final_state) - if self.num_trajectories == 1: - self._sum_final_states_no_jump += dm_final_state - else: - self._sum_final_states_jump += dm_final_state - - def _average_computer(self): - avg = np.array(self._sum_expect_jump) / (self.num_trajectories - 1) - avg2 = np.array(self._sum2_expect_jump) / (self.num_trajectories - 1) - return avg, avg2 - - def _add_first_traj(self, trajectory): - super()._add_first_traj(trajectory) - if trajectory.states and self._store_average_density_matricies: - del self._sum_states - self._sum_states_no_jump = [ - qzero_like(ket2dm(state)) for state in trajectory.states - ] - self._sum_states_jump = [ - qzero_like(ket2dm(state)) for state in trajectory.states - ] - if trajectory.final_state and self._store_final_density_matrix: - state = trajectory.final_state - del self._sum_final_states - self._sum_final_states_no_jump = qzero_like(ket2dm(state)) - self._sum_final_states_jump = qzero_like(ket2dm(state)) - self._sum_expect_jump = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - self._sum2_expect_jump = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - self._sum_expect_no_jump = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - self._sum2_expect_no_jump = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - self._sum_expect_jump = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - self._sum2_expect_jump = [ - np.zeros_like(expect) for expect in trajectory.expect - ] - del self._sum_expect - del self._sum2_expect - - def _reduce_expect(self, trajectory): - """ - Compute the average of the expectation values appropriately - weighting the jump and no-jump trajectories - """ - for i, k in enumerate(self._raw_ops): - expect_traj = trajectory.expect[i] - p = self.no_jump_prob - if self.num_trajectories == 1: - self._sum_expect_no_jump[i] += expect_traj * p - self._sum2_expect_no_jump[i] += expect_traj**2 * p - # no jump trajectory will always be the first one, no need - # to worry about including jump trajectories - avg = self._sum_expect_no_jump[i] - avg2 = self._sum2_expect_no_jump[i] - else: - self._sum_expect_jump[i] += expect_traj * (1 - p) - self._sum2_expect_jump[i] += expect_traj**2 * (1 - p) - avg = self._sum_expect_no_jump[i] + ( - self._sum_expect_jump[i] / (self.num_trajectories - 1) - ) - avg2 = self._sum2_expect_no_jump[i] + ( - self._sum2_expect_jump[i] / (self.num_trajectories - 1) - ) - - self.average_e_data[k] = list(avg) - - # mean(expect**2) - mean(expect)**2 can something be very small - # negative (-1e-15) which raise an error for float sqrt. - self.std_e_data[k] = list(np.sqrt(np.abs(avg2 - np.abs(avg**2)))) - - if self.runs_e_data: - self.runs_e_data[k].append(trajectory.e_data[k]) - - @property - def average_states(self): - """ - States averages as density matrices. - """ - if self._sum_states_no_jump is None: - if not (self.trajectories and self.trajectories[0].states): - return None - self._sum_states_no_jump = [ - qzero_like(ket2dm(state)) - for state in self.trajectories[0].states - ] - self._sum_states_jump = [ - qzero_like(ket2dm(state)) - for state in self.trajectories[0].states - ] - self.num_trajectories = 0 - for trajectory in self.trajectories: - self.num_trajectories += 1 - self._reduce_states(trajectory) - p = self.no_jump_prob - return [ - p * final_no_jump - + (1 - p) * final_jump / (self.num_trajectories - 1) - for final_no_jump, final_jump in zip( - self._sum_states_no_jump, self._sum_states_jump - ) - ] - - @property - def average_final_state(self): - """ - Last states of each trajectory averaged into a density matrix. - """ - if self._sum_final_states_no_jump is None: - if self.average_states is not None: - return self.average_states[-1] - p = self.no_jump_prob - return p * self._sum_final_states_no_jump + ( - ((1 - p) * self._sum_final_states_jump) - / (self.num_trajectories - 1) - ) - - def __add__(self, other): - raise NotImplemented - - @property - def photocurrent(self): - """ - Average photocurrent or measurement of the evolution. - """ - cols = [[] for _ in range(self.num_c_ops)] - tlist = self.times - for collapses in self.collapse: - for t, which in collapses: - cols[which].append(t) - mesurement = [ - (1 - self.no_jump_prob) - / (self.num_trajectories - 1) - * np.histogram(cols[i], tlist)[0] - / np.diff(tlist) - for i in range(self.num_c_ops) - ] - return mesurement +def _to_dm(state): + if state.type == "ket": + state = state.proj() + return state diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index ee35c63f57..af966edcf8 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,7 +1,7 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem -from .result import MultiTrajResult, Result, ExpectOp +from .result import MultiTrajResult, ExpectOp, TrajectoryResult from .multitraj import _MultiTrajRHS, MultiTrajSolver from .. import Qobj, QobjEvo from ..core.dimensions import Dimensions @@ -11,7 +11,8 @@ from ._feedback import _QobjFeedback, _DataFeedback, _WienerFeedback -class StochasticTrajResult(Result): +class StochasticTrajResult(TrajectoryResult): + # TODO double check this def _post_init(self, m_ops=(), dw_factor=(), heterodyne=False): super()._post_init() self.W = [] diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index e47b638e4b..60ef464109 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -372,12 +372,12 @@ def test_states_outputs(keep_runs_results): assert len(data.runs_states[0]) == len(times) assert isinstance(data.runs_states[0][0], qutip.Qobj) assert data.runs_states[0][0].norm() == pytest.approx(1.) - assert data.runs_states[0][0].isoper + assert data.runs_states[0][0].isket assert len(data.runs_final_states) == ntraj assert isinstance(data.runs_final_states[0], qutip.Qobj) assert data.runs_final_states[0].norm() == pytest.approx(1.) - assert data.runs_final_states[0].isoper + assert data.runs_final_states[0].isket assert isinstance(data.steady_state(), qutip.Qobj) assert data.steady_state().norm() == pytest.approx(1.) diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index a4830460ef..a617149c08 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -2,7 +2,9 @@ import pytest import qutip -from qutip.solver.result import Result, MultiTrajResult, McResult +from qutip.solver.result import ( + Result, MultiTrajResult, McResult, TrajectoryResult +) def fill_options(**kwargs): @@ -173,7 +175,7 @@ def _fill_trajectories(self, multiresult, N, ntraj, # Fix the seed to avoid failing due to bad luck np.random.seed(1) for _ in range(ntraj): - result = Result(multiresult._raw_ops, multiresult.options) + result = TrajectoryResult(multiresult._raw_ops, multiresult.options) result.collapse = [] for t in range(N): delta = 1 + noise * np.random.randn() From 4b81f970a04c269c1cb1593276d4b0c398a21f9b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 29 Mar 2024 11:36:35 +0900 Subject: [PATCH 060/305] Small fixes --- doc/apidoc/classes.rst | 8 ++++++-- qutip/solver/nm_mcsolve.py | 2 +- qutip/solver/result.py | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index 375c71542b..3404616a22 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -237,11 +237,15 @@ Solver Options and Results :exclude-members: add_processor, add, add_end_condition .. autoclass:: qutip.solver.result.TrajectoryResult + :show-inheritance: + :members: .. autoclass:: qutip.solver.result.McResult + :show-inheritance: :members: - :inherited-members: - :exclude-members: add_processor, add, add_end_condition + +.. autoclass:: qutip.solver.result.NmmcResult + :show-inheritance: :members: .. _classes-piqs: diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index a6c8473b77..397873a88c 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -527,7 +527,7 @@ def step(self, t, *, args=None, copy=True): if isket(state): state = ket2dm(state) return state * self.current_martingale() - + def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ Run one trajectory and return the result. diff --git a/qutip/solver/result.py b/qutip/solver/result.py index cfa3be64e0..9c1314ff91 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -379,6 +379,19 @@ class MultiTrajResult(_BaseResult): Parameters ---------- + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these + The ``e_ops`` parameter defines the set of values to record at + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that + operator given the state at ``t``. If the element is a function, ``f``, + the value recorded is ``f(t, state)``. + + The values are recorded in the ``.expect`` attribute of this result + object. ``.expect`` is a list, where each item contains the values + of the corresponding ``e_op``. + + Function ``e_ops`` must return a number so the average can be computed. + options : dict The options for this result class. @@ -642,6 +655,7 @@ def _target_tolerance_end(self): Return the approximate number of trajectories needed to have this error within the tolerance fot all e_ops and times. """ + # TODO this might be wrong now if self.num_trajectories <= 1: return np.inf avg, avg2 = self._average_computer() @@ -1314,7 +1328,6 @@ def _add_trace(self, trajectory): self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) if self.options["keep_runs_results"]: - # TODO rename this to runs_martingales? self.runs_trace.append(trajectory.trace) @property From 4a79860cf4b65fb6c33933dedc85366806d33111 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 29 Mar 2024 13:23:36 +0900 Subject: [PATCH 061/305] More small fixes --- qutip/solver/result.py | 43 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 9c1314ff91..c309364e18 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -411,14 +411,6 @@ class MultiTrajResult(_BaseResult): A list of the times at which the expectation values and states were recorded. - e_ops : dict - A dictionary containing the supplied e_ops as ``ExpectOp`` instances. - The keys of the dictionary are the same as for ``.e_data``. - Each value is object where ``.e_ops[k](t, state)`` calculates the - value of ``e_op`` ``k`` at time ``t`` and the given ``state``, and - ``.e_ops[k].op`` is the original object supplied to create the - ``e_op``. - average_states : list of :obj:`.Qobj` The state at each time ``t`` (if the recording of the state was requested) averaged over all trajectories as a density matrix. @@ -518,7 +510,10 @@ def __init__( self.average_e_data = {} self.std_e_data = {} - self.runs_e_data = {} + if self.options["keep_runs_results"]: + self.runs_e_data = {k: [] for k in self._raw_ops} + else: + self.runs_e_data = {} # Will be initialized at the first trajectory self.times = None @@ -536,8 +531,8 @@ def __init__( @property def _store_average_density_matrices(self) -> bool: return ( - self.options["store_states"] or - (self.options["store_states"] is None and len(self._raw_ops) == 0) + self.options["store_states"] + or (self.options["store_states"] is None and self._raw_ops == {}) ) and not self.options["keep_runs_results"] @property @@ -554,9 +549,6 @@ def _add_first_traj(self, trajectory): """ self.times = trajectory.times - if self.options["keep_runs_results"]: - self.runs_e_data = {k: [] for k in self._raw_ops} - def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) @@ -655,7 +647,7 @@ def _target_tolerance_end(self): Return the approximate number of trajectories needed to have this error within the tolerance fot all e_ops and times. """ - # TODO this might be wrong now + # TODO update this function for weighted trajectories if self.num_trajectories <= 1: return np.inf avg, avg2 = self._average_computer() @@ -685,7 +677,7 @@ def _post_init(self): self.add_processor(self._reduce_states) if self._store_final_density_matrix: self.add_processor(self._reduce_final_state) - if self.e_ops: + if self._raw_ops: self.add_processor(self._reduce_expect) self.stats["end_condition"] = "unknown" @@ -751,7 +743,7 @@ def add_end_condition(self, ntraj, target_tol=None): self._early_finish_check = self._fixed_end return - num_e_ops = len(self.e_ops) + num_e_ops = len(self._raw_ops) if not num_e_ops: raise ValueError("Cannot target a tolerance without e_ops") @@ -845,11 +837,12 @@ def average_final_state(self): return average_states[-1] return None + + if self._sum_abs and self._sum_rel: + return (self._sum_abs.sum_final_state + + self._sum_rel.sum_final_state / self._num_rel_trajectories) if self._sum_rel: - result = self._sum_rel.sum_final_state / self._num_rel_trajectories - if self._sum_abs: - result += self._sum_abs.sum_final_state - return result + return self._sum_rel.sum_final_state / self._num_rel_trajectories return self._sum_abs.sum_final_state @property @@ -929,7 +922,7 @@ def __repr__(self): def __add__(self, other): if not isinstance(other, MultiTrajResult): raise NotImplementedError("Can only add two multi traj results") - if self.e_ops != other.e_ops: + if self._raw_ops != other._raw_ops: raise ValueError("Shared `e_ops` is required to merge results") if self.times != other.times: raise ValueError("Shared `times` are is required to merge results") @@ -947,6 +940,8 @@ def __add__(self, other): new.seeds = self.seeds + other.seeds if self._sum_abs: + # TODO this does not work for two results, + # each containing a no-jump trajectory new._sum_abs = self._sum_abs + other._sum_abs if self._sum_rel: new._sum_rel = self._sum_rel + other._sum_rel @@ -1298,8 +1293,8 @@ def _post_init(self): self._sum2_trace_abs = None self._sum2_trace_rel = None - self.average_trace = None - self.std_trace = None + self.average_trace = [] + self.std_trace = [] self.runs_trace = [] self.add_processor(self._add_trace) From 24c795223b22ad941e6cd2525816fbb2e5a6e4e1 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 29 Mar 2024 16:00:38 +0900 Subject: [PATCH 062/305] Code formatting --- qutip/solver/result.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index c309364e18..d32fdfd634 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -519,7 +519,7 @@ def __init__( self.times = None # We separate all sums into terms of trajectories with specified - # absolute weight (`_abs`) or without (`_rel`). They will be initialized + # absolute weight (_abs) or without (_rel). They will be initialized # when the first trajectory of the respective type is added. self._sum_rel = None self._sum_abs = None @@ -804,8 +804,7 @@ def average_states(self): ] if self._sum_rel: return [r / self._num_rel_trajectories - for r in self._sum_rel.sum_states - ] + for r in self._sum_rel.sum_states] return self._sum_abs.sum_states @property @@ -830,14 +829,13 @@ def average_final_state(self): """ Last states of each trajectories averaged into a density matrix. """ - if ((self._sum_abs and not self._sum_abs.sum_final_state) - or (self._sum_rel and not self._sum_rel.sum_final_state)): + if ((self._sum_abs and not self._sum_abs.sum_final_state) or + (self._sum_rel and not self._sum_rel.sum_final_state)): if (average_states := self.average_states) is not None: return average_states[-1] return None - if self._sum_abs and self._sum_rel: return (self._sum_abs.sum_final_state + self._sum_rel.sum_final_state / self._num_rel_trajectories) @@ -980,8 +978,7 @@ def __init__(self, example_trajectory, store_states, store_final_state): def _initialize_sum_states(self, example_trajectory): self.sum_states = [ - qzero_like(_to_dm(state)) for state in example_trajectory.states - ] + qzero_like(_to_dm(state)) for state in example_trajectory.states] def reduce_states(self, trajectory): if trajectory.has_weight(): From 72aac7859e93174a8cb484216f245c7af280defb Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Fri, 29 Mar 2024 19:41:35 +0530 Subject: [PATCH 063/305] fix the negativity function --- qutip/entropy.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qutip/entropy.py b/qutip/entropy.py index 6f3f5cc7a6..813201f018 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -4,7 +4,8 @@ from numpy import conj, e, inf, imag, inner, real, sort, sqrt from numpy.lib.scimath import log, log2 -from . import (ptrace, ket2dm, tensor, sigmay, partial_transpose, +from qutip import partial_transpose, ket2dm +from . import (ptrace, tensor, sigmay, expand_operator) from .core import data as _data @@ -129,6 +130,10 @@ def negativity(rho, subsys, method='tracenorm', logarithmic=False): Experimental. """ + + if rho.isket: + rho = ket2dm(rho) + mask = [idx == subsys for idx, n in enumerate(rho.dims[0])] rho_pt = partial_transpose(rho, mask) From d446ae5522c6e7fcb0e584b622c9582c79470658 Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Fri, 29 Mar 2024 23:49:35 +0530 Subject: [PATCH 064/305] Remove the spaces --- qutip/entropy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutip/entropy.py b/qutip/entropy.py index 813201f018..06282c4de7 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -130,10 +130,8 @@ def negativity(rho, subsys, method='tracenorm', logarithmic=False): Experimental. """ - if rho.isket: rho = ket2dm(rho) - mask = [idx == subsys for idx, n in enumerate(rho.dims[0])] rho_pt = partial_transpose(rho, mask) From 62228cf66e091bc7186cd0b61fcc6209797e92d0 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Sun, 31 Mar 2024 15:19:38 +0900 Subject: [PATCH 065/305] Update master version number --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 6f5f6c392d..5ef8a7e04a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.0.0.dev +5.1.0.dev From e55c084079082c6a01c4591403fb162b45f932cf Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 1 Apr 2024 10:06:38 +0900 Subject: [PATCH 066/305] Add equation in the guide for open mcsolve evolution --- doc/guide/dynamics/dynamics-monte.rst | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/doc/guide/dynamics/dynamics-monte.rst b/doc/guide/dynamics/dynamics-monte.rst index 4ca406bf90..8724d94f9d 100644 --- a/doc/guide/dynamics/dynamics-monte.rst +++ b/doc/guide/dynamics/dynamics-monte.rst @@ -445,6 +445,29 @@ Open Systems ``mcsolve`` can be used to study systems which have measurement and dissipative interactions with their environment. This is done by passing a Liouvillian including the dissipative interaction to the solver instead of a Hamiltonian. +In that cases the effective Liouvillian becomes: + +.. math:: + :label: Leff + + L_{\rm eff}\rho = L_{\rm sys}\rho -\frac{1}{2}\sum_{i}\left( C^{+}_{n}C_{n}\rho + \rho C^{+}_{n}C_{n}\right), + +With the collapse probability becoming: + +.. math:: + :label: L_jump + + \delta p =\delta t \sum_{n}\mathrm{tr}\left(\rho(t)C^{+}_{n}C_{n}\right), + +And a jump with the collapse ``n`` changing the state as: + +.. math:: + :label: L_project + + \rho(t+\delta t) = C_{n} \rho(t) C^{+}_{n} / \mathrm{tr}\left( C_{n} \rho(t) C^{+}_{n} \right), + + +We can redo the previous example for a situation where only half the emitted photons are detected. .. plot:: :context: close-figs @@ -454,8 +477,8 @@ dissipative interaction to the solver instead of a Hamiltonian. a = tensor(qeye(2), destroy(10)) sm = tensor(destroy(2), qeye(10)) H = 2*np.pi*a.dag()*a + 2*np.pi*sm.dag()*sm + 2*np.pi*0.25*(sm*a.dag() + sm.dag()*a) - L = liouvillian(H, [0.01 * sm, np.sqrt(0.1) * a]) - data = mcsolve(L, psi0, times, [np.sqrt(0.1) * a], e_ops=[a.dag() * a, sm.dag() * sm]) + L = liouvillian(H, [np.sqrt(0.05) * a]) + data = mcsolve(L, psi0, times, [np.sqrt(0.05) * a], e_ops=[a.dag() * a, sm.dag() * sm]) plt.figure() plt.plot((times[:-1] + times[1:])/2, data.photocurrent[0]) From 34e4ef462add79be3575a5e175cc8f78a71c913a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 1 Apr 2024 13:17:13 +0900 Subject: [PATCH 067/305] Ensure progress bars don't break on too many update --- qutip/tests/test_progressbar.py | 24 ++++++++++++++++++++++++ qutip/ui/progressbar.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/qutip/tests/test_progressbar.py b/qutip/tests/test_progressbar.py index d1fa275f17..3445ebd2a3 100644 --- a/qutip/tests/test_progressbar.py +++ b/qutip/tests/test_progressbar.py @@ -34,6 +34,30 @@ def test_progressbar(pbar): assert bar.total_time() > 0 +@pytest.mark.parametrize("pbar", bars) +def test_progressbar_too_few_update(pbar): + N = 5 + bar = progress_bars[pbar](N) + assert bar.total_time() < 0 + for _ in range(N-2): + time.sleep(0.01) + bar.update() + bar.finished() + assert bar.total_time() > 0 + + +@pytest.mark.parametrize("pbar", bars) +def test_progressbar_too_many_update(pbar): + N = 5 + bar = progress_bars[pbar](N) + assert bar.total_time() < 0 + for _ in range(N+2): + time.sleep(0.01) + bar.update() + bar.finished() + assert bar.total_time() > 0 + + @pytest.mark.parametrize("pbar", bars[1:]) def test_progressbar_has_print(pbar, capsys): N = 2 diff --git a/qutip/ui/progressbar.py b/qutip/ui/progressbar.py index 1999d0bf8f..7bb8d8c962 100644 --- a/qutip/ui/progressbar.py +++ b/qutip/ui/progressbar.py @@ -44,7 +44,7 @@ def time_elapsed(self): return "%6.2fs" % (time.time() - self.t_start) def time_remaining_est(self, p): - if p > 0.0: + if 100 >= p > 0.0: t_r_est = (time.time() - self.t_start) * (100.0 - p) / p else: t_r_est = 0 From ab87d6fce4c6d51914d60bbd7ceaeaeacd4e395a Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 1 Apr 2024 14:20:05 +0900 Subject: [PATCH 068/305] Suggestions from code review --- qutip/solver/result.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index d32fdfd634..2e5c4466ca 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -501,7 +501,6 @@ def __init__( solver=None, stats=None, **kw, ): super().__init__(options, solver=solver, stats=stats) - self.e_ops = e_ops or [] self._raw_ops = self._e_ops_to_dict(e_ops) self.trajectories = [] @@ -517,6 +516,7 @@ def __init__( # Will be initialized at the first trajectory self.times = None + self.e_ops = None # We separate all sums into terms of trajectories with specified # absolute weight (_abs) or without (_rel). They will be initialized @@ -548,6 +548,7 @@ def _add_first_traj(self, trajectory): Read the first trajectory, intitializing needed data. """ self.times = trajectory.times + self.e_ops = trajectory.e_ops def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) @@ -919,16 +920,17 @@ def __repr__(self): def __add__(self, other): if not isinstance(other, MultiTrajResult): - raise NotImplementedError("Can only add two multi traj results") + return NotImplemented if self._raw_ops != other._raw_ops: raise ValueError("Shared `e_ops` is required to merge results") if self.times != other.times: raise ValueError("Shared `times` are is required to merge results") new = self.__class__( - self.e_ops, self.options, solver=self.solver, stats=self.stats + self._raw_ops, self.options, solver=self.solver, stats=self.stats ) new.times = self.times + new.e_ops = self.e_ops if self.trajectories and other.trajectories: new.trajectories = self.trajectories + other.trajectories @@ -1015,7 +1017,7 @@ def __add__(self, other): if other is None: return self if not isinstance(other, _TrajectorySum): - raise NotImplementedError("Can only add two trajectory sums") + return NotImplemented new = copy(self) From ada5c04b1c1ad46c956d1e4dbdd37a67135f6afa Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 1 Apr 2024 15:19:48 +0900 Subject: [PATCH 069/305] photocurrent with weights --- qutip/solver/result.py | 120 ++++++++++++++++++++++++++--------------- 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 2e5c4466ca..f4bfdb90e0 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -554,13 +554,13 @@ def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) def _reduce_states(self, trajectory): - if trajectory.has_absolute_weight(): + if trajectory.has_absolute_weight: self._sum_abs.reduce_states(trajectory) else: self._sum_rel.reduce_states(trajectory) def _reduce_final_state(self, trajectory): - if trajectory.has_absolute_weight(): + if trajectory.has_absolute_weight: self._sum_abs.reduce_final_state(trajectory) else: self._sum_rel.reduce_final_state(trajectory) @@ -570,7 +570,7 @@ def _reduce_expect(self, trajectory): Compute the average of the expectation values and store it in it's multiple formats. """ - if trajectory.has_absolute_weight(): + if trajectory.has_absolute_weight: self._sum_abs.reduce_expect(trajectory) else: self._sum_rel.reduce_expect(trajectory) @@ -605,7 +605,7 @@ def _increment_traj(self, trajectory): if self.num_trajectories == 0: self._add_first_traj(trajectory) - if trajectory.has_absolute_weight(): + if trajectory.has_absolute_weight: if self._sum_abs is None: self._sum_abs = _TrajectorySum( trajectory, @@ -983,12 +983,12 @@ def _initialize_sum_states(self, example_trajectory): qzero_like(_to_dm(state)) for state in example_trajectory.states] def reduce_states(self, trajectory): - if trajectory.has_weight(): + if trajectory.has_weight: self.sum_states = [ accu + weight * _to_dm(state) for accu, state, weight in zip(self.sum_states, trajectory.states, - trajectory._total_weight()) + trajectory._total_weight_tlist) ] else: self.sum_states = [ @@ -997,18 +997,14 @@ def reduce_states(self, trajectory): ] def reduce_final_state(self, trajectory): - if trajectory.has_weight(): - self.sum_final_state += (trajectory._final_weight() * + if trajectory.has_weight: + self.sum_final_state += (trajectory._final_weight * _to_dm(trajectory.final_state)) else: self.sum_final_state += _to_dm(trajectory.final_state) def reduce_expect(self, trajectory): - if trajectory.has_weight(): - weight = trajectory._total_weight() - else: - weight = 1 - + weight = trajectory.total_weight for i, expect_traj in enumerate(trajectory.expect): self.sum_expect[i] += weight * expect_traj self.sum2_expect[i] += weight * expect_traj**2 @@ -1101,28 +1097,51 @@ def add_relative_weight(self, new_weight): self.rel_weight = self.rel_weight * new_weight self._has_weight = True + @property def has_weight(self): """Whether any weight has been set.""" return self._has_weight + @property def has_absolute_weight(self): """Whether an absolute weight has been set.""" return (self.abs_weight is not None) - def _total_weight(self): + @property + def has_time_dependent_weight(self): + """Whether the total weight is time-dependent.""" + # np.ndim(None) returns zero, which is what we want + return np.ndim(self.rel_weight) > 0 or np.ndim(self.abs_weight) > 0 + + @property + def total_weight(self): + """ + Returns the total weight, either a single number or an array in case of + a time-dependent weight. If no absolute weight was set, this is only + the relative weight. If an absolute weight was set, this is the product + of the absolute and the relative weights. + """ + if self.has_absolute_weight: + return self.abs_weight * self.rel_weight + return self.rel_weight + + @property + def _total_weight_tlist(self): """ - Returns an array containing the weight as a function of time. If no - absolute weight was set, this is only the relative weight. If an - absolute weight was set, this is the product of abs and rel. + Returns the total weight as a function of time (i.e., as an array with + the same shape as the `tlist`) """ - weights = np.ones_like(self.times) - weights = weights * self.rel_weight - if self.abs_weight: - weights = weights * self.abs_weight - return weights + total_weight = self.total_weight + if self.has_time_dependent_weight: + return total_weight + return np.ones_like(self.times) * total_weight + @property def _final_weight(self): - return self._total_weight()[-1] + total_weight = self.total_weight + if self.has_time_dependent_weight: + return total_weight[-1] + return total_weight class McResult(MultiTrajResult): @@ -1164,16 +1183,26 @@ class McResult(MultiTrajResult): """ # Collapse are only produced by mcsolve. - def _add_collapse(self, trajectory): self.collapse.append(trajectory.collapse) + # Need to store weights of trajectories to compute photocurrent + def _store_weight(self, trajectory): + self.runs_weights.append(trajectory.total_weight) + if trajectory.has_time_dependent_weight: + self._time_dependent_weights = True + def _post_init(self): super()._post_init() self.num_c_ops = self.stats["num_collapse"] + self.collapse = [] self.add_processor(self._add_collapse) + self.runs_weights = [] + self._time_dependent_weights = False + self.add_processor(self._store_weight) + @property def col_times(self): """ @@ -1203,18 +1232,23 @@ def photocurrent(self): """ Average photocurrent or measurement of the evolution. """ - # TODO this is wrong if trajectories have weights - # unclear how to implement in case of time-dependent weights - cols = [[] for _ in range(self.num_c_ops)] + if self._time_dependent_weights: + raise NotImplementedError("photocurrent is not implemented " + "for this solver.") + + collapse_times = [[] for _ in range(self.num_c_ops)] + collapse_weights = [[] for _ in range(self.num_c_ops)] tlist = self.times - for collapses in self.collapse: + for collapses, weight in zip(self.collapse, self.runs_weights): for t, which in collapses: - cols[which].append(t) + collapse_times[which].append(t) + collapse_weights[which].append(weight) + mesurement = [ - np.histogram(cols[i], tlist)[0] + np.histogram(times, bins=tlist, weights=weights)[0] / np.diff(tlist) / self.num_trajectories - for i in range(self.num_c_ops) + for times, weights in zip(collapse_times, collapse_weights) ] return mesurement @@ -1223,18 +1257,20 @@ def runs_photocurrent(self): """ Photocurrent or measurement of each runs. """ - # TODO this is wrong if trajectories have weights - # unclear how to implement in case of time-dependent weights + if self._time_dependent_weights: + raise NotImplementedError("runs_photocurrent is not implemented " + "for this solver.") + tlist = self.times measurements = [] for collapses in self.collapse: - cols = [[] for _ in range(self.num_c_ops)] + collapse_times = [[] for _ in range(self.num_c_ops)] for t, which in collapses: - cols[which].append(t) + collapse_times[which].append(t) measurements.append( [ - np.histogram(cols[i], tlist)[0] / np.diff(tlist) - for i in range(self.num_c_ops) + np.histogram(times, tlist)[0] / np.diff(tlist) + for times in collapse_times ] ) return measurements @@ -1306,12 +1342,12 @@ def _add_first_traj(self, trajectory): self._sum2_trace_rel = np.zeros_like(trajectory.times) def _add_trace(self, trajectory): - if trajectory.has_absolute_weight(): - self._sum_trace_abs += trajectory._total_weight() - self._sum2_trace_abs += np.abs(trajectory._total_weight()) ** 2 + if trajectory.has_absolute_weight: + self._sum_trace_abs += trajectory._total_weight_tlist + self._sum2_trace_abs += np.abs(trajectory._total_weight_tlist) ** 2 else: - self._sum_trace_rel += trajectory._total_weight() - self._sum2_trace_rel += np.abs(trajectory._total_weight()) ** 2 + self._sum_trace_rel += trajectory._total_weight_tlist + self._sum2_trace_rel += np.abs(trajectory._total_weight_tlist) ** 2 avg = (self._sum_trace_abs + self._sum_trace_rel / self._num_rel_trajectories) From 66c2ce6690b6fc470d64b50886bf8e63ab24bef9 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 1 Apr 2024 15:32:45 +0900 Subject: [PATCH 070/305] Removed photocurrent from nm_mcsolve tests --- qutip/tests/solver/test_nm_mcsolve.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index 60ef464109..1dc64b2968 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -360,13 +360,6 @@ def test_states_outputs(keep_runs_results): assert data.average_final_state.norm() == pytest.approx(1.) assert data.average_final_state.isoper - assert isinstance(data.photocurrent[0][1], float) - assert isinstance(data.photocurrent[1][1], float) - assert ( - np.array(data.runs_photocurrent).shape - == (ntraj, total_ops, len(times)-1) - ) - if keep_runs_results: assert len(data.runs_states) == ntraj assert len(data.runs_states[0]) == len(times) @@ -431,10 +424,6 @@ def test_expectation_outputs(keep_runs_results): assert isinstance(data.runs_expect[0][0][1], float) assert isinstance(data.runs_expect[1][0][1], float) assert isinstance(data.runs_expect[2][0][1], complex) - assert isinstance(data.photocurrent[0][0], float) - assert isinstance(data.photocurrent[1][0], float) - assert (np.array(data.runs_photocurrent).shape - == (ntraj, total_ops, len(times)-1)) np.testing.assert_allclose(times, data.times) assert data.num_trajectories == ntraj assert len(data.e_ops) == len(e_ops) From b059661253f06d1dc96c9afa20049144c317ceff Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 1 Apr 2024 16:37:20 +0900 Subject: [PATCH 071/305] Revert stochastic solvers to Result instead of TrajectoryResult --- qutip/solver/result.py | 8 ++++++++ qutip/solver/stochastic.py | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index f4bfdb90e0..78140c9373 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -707,6 +707,14 @@ def add(self, trajectory_info): seed, trajectory = trajectory_info self.seeds.append(seed) + if not isinstance(trajectory, TrajectoryResult): + trajectory.has_weight = False + trajectory.has_absolute_weight = False + trajectory.has_time_dependent_weight = False + trajectory.total_weight = 1 + trajectory._total_weight_tlist = np.ones_like(trajectory.times) + trajectory._final_weight = 1 + for op in self._state_processors: op(trajectory) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index af966edcf8..ee35c63f57 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,7 +1,7 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem -from .result import MultiTrajResult, ExpectOp, TrajectoryResult +from .result import MultiTrajResult, Result, ExpectOp from .multitraj import _MultiTrajRHS, MultiTrajSolver from .. import Qobj, QobjEvo from ..core.dimensions import Dimensions @@ -11,8 +11,7 @@ from ._feedback import _QobjFeedback, _DataFeedback, _WienerFeedback -class StochasticTrajResult(TrajectoryResult): - # TODO double check this +class StochasticTrajResult(Result): def _post_init(self, m_ops=(), dw_factor=(), heterodyne=False): super()._post_init() self.W = [] From b7eadd7194e1321424756fa03e45f58ef434e31e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 1 Apr 2024 16:42:52 +0900 Subject: [PATCH 072/305] Towncrier entry --- doc/changes/2369.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2369.feature diff --git a/doc/changes/2369.feature b/doc/changes/2369.feature new file mode 100644 index 0000000000..3eb02eb8a9 --- /dev/null +++ b/doc/changes/2369.feature @@ -0,0 +1 @@ +Weighted trajectories in trajectory solvers (enables improved sampling for nm_mcsolve) \ No newline at end of file From fedb376222292489aecd98623afe419498f6ec1b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 1 Apr 2024 19:08:41 +0900 Subject: [PATCH 073/305] Merging results with weighted trajectories, and mcsolve and nm_mcsolve results. --- qutip/solver/result.py | 187 +++++++++++++++++++++++++++++++------ qutip/solver/stochastic.py | 6 ++ 2 files changed, 166 insertions(+), 27 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 78140c9373..ae513a203d 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -926,7 +926,31 @@ def __repr__(self): lines.append(">") return "\n".join(lines) - def __add__(self, other): + def merge(self, other, p=None): + r""" + Merges two multi-trajectory results. + + If this result represent an ensemble :math:`\rho`, and `other` + represents an ensemble :math:`\rho'`, then the merged result + represents the ensemble + + .. math:: + \rho_{\mathrm{merge}} = p \rho + (1 - p) \rho' + + where p is a parameter between 0 and 1. Its default value is + :math:`p_{\textrm{def}} = N / (N + N')`, N and N' being the number of + trajectories in the two result objects. (In the case of weighted + trajectories, only trajectories without absolute weights are counted.) + + Parameters + ---------- + other : MultiTrajResult + The multi-trajectory result to merge with this one + p : float [optional] + The relative weight of this result in the combination. By default, + will be chosen such that all trajectories contribute equally + to the merged result. + """ if not isinstance(other, MultiTrajResult): return NotImplemented if self._raw_ops != other._raw_ops: @@ -940,19 +964,27 @@ def __add__(self, other): new.times = self.times new.e_ops = self.e_ops - if self.trajectories and other.trajectories: - new.trajectories = self.trajectories + other.trajectories new.num_trajectories = self.num_trajectories + other.num_trajectories new._num_rel_trajectories = (self._num_rel_trajectories + other._num_rel_trajectories) new.seeds = self.seeds + other.seeds - if self._sum_abs: - # TODO this does not work for two results, - # each containing a no-jump trajectory - new._sum_abs = self._sum_abs + other._sum_abs - if self._sum_rel: - new._sum_rel = self._sum_rel + other._sum_rel + p_default = self._num_rel_trajectories / new._num_rel_trajectories + if p is None: + p_is_default = True + p = p_default + else: + p_is_default = False + + if self.trajectories and other.trajectories: + new.trajectories = self._merge_trajectories( + other, p, p_default, p_is_default) + + new._sum_abs = _TrajectorySum.merge( + self._sum_abs, p, other._sum_abs, 1 - p) + new._sum_rel = _TrajectorySum.merge( + self._sum_rel, p / p_default, + other._sum_rel, (1 - p) / (1 - p_default)) new._create_e_data() @@ -966,6 +998,38 @@ def __add__(self, other): return new + def _merge_trajectories(self, other, p, p_default, p_is_default): + # Due to how the weights work, we may have to add weights to the + # trajectories of the merged result. Only if p has the default value + # and there are no absolute weights present, we do not have to do any + # extra work + if (p_is_default and + self.num_trajectories == self._num_rel_trajectories and + other.num_trajectories == other._num_rel_trajectories): + return self.trajectories + other.trajectories + + result = [] + for traj in self.trajectories: + if traj.has_absolute_weight: + traj = copy(traj) + traj.add_absolute_weight(p) + elif not p_is_default: + traj = copy(traj) + traj.add_relative_weight(p / p_default) + result.append(traj) + for traj in other.trajectories: + if traj.has_absolute_weight: + traj = copy(traj) + traj.add_absolute_weight(1 - p) + elif not p_is_default: + traj = copy(traj) + traj.add_relative_weight((1 - p) / (1 - p_default)) + result.append(traj) + return result + + def __add__(self, other): + return self.merge(other, p=None) + class _TrajectorySum: def __init__(self, example_trajectory, store_states, store_final_state): @@ -1017,33 +1081,47 @@ def reduce_expect(self, trajectory): self.sum_expect[i] += weight * expect_traj self.sum2_expect[i] += weight * expect_traj**2 - def __add__(self, other): - if other is None: - return self - if not isinstance(other, _TrajectorySum): - return NotImplemented + @staticmethod + def merge(sum1, weight1, sum2, weight2): + if sum1 is None and sum2 is None: + return None + if sum1 is None: + return _TrajectorySum.merge(sum2, weight2, sum1, weight1) - new = copy(self) + new = copy(sum1) - if self.sum_states and other.sum_states: + if sum2 is None: + if sum1.sum_states: + new.sum_states = [ + weight1 * state1 for state1 in sum1.sum_states + ] + if sum1.sum_final_state: + new.sum_final_state = weight1 * sum1.sum_final_state + new.sum_expect = [weight1 * e1 for e1 in sum1.sum_expect] + new.sum2_expect = [weight1 * e1 for e1 in sum1.sum2_expect] + return new + + if sum1.sum_states and sum2.sum_states: new.sum_states = [ - state1 + state2 for state1, state2 in zip( - self.sum_states, other.sum_states + weight1 * state1 + weight2 * state2 for state1, state2 in zip( + sum1.sum_states, sum2.sum_states ) ] else: new.sum_states = None - if self.sum_final_state and other.sum_final_state: - new.sum_final_state += other.sum_final_state + if sum1.sum_final_state and sum2.sum_final_state: + new.sum_final_state = ( + weight1 * sum1.sum_final_state + + weight2 * sum2.sum_final_state) else: new.sum_final_state = None - new.sum_expect = [sum1 + sum2 for sum1, sum2 in zip( - self.sum_expect, other.sum_expect) + new.sum_expect = [weight1 * e1 + weight2 * e2 for e1, e2 in zip( + sum1.sum_expect, sum2.sum_expect) ] - new.sum2_expect = [sum1 + sum2 for sum1, sum2 in zip( - self.sum2_expect, other.sum2_expect) + new.sum2_expect = [weight1 * e1 + weight2 * e2 for e1, e2 in zip( + sum1.sum2_expect, sum2.sum2_expect) ] return new @@ -1197,6 +1275,7 @@ def _add_collapse(self, trajectory): # Need to store weights of trajectories to compute photocurrent def _store_weight(self, trajectory): self.runs_weights.append(trajectory.total_weight) + self._weights_absolute.append(trajectory.has_absolute_weight) if trajectory.has_time_dependent_weight: self._time_dependent_weights = True @@ -1208,6 +1287,7 @@ def _post_init(self): self.add_processor(self._add_collapse) self.runs_weights = [] + self._weights_absolute = [] self._time_dependent_weights = False self.add_processor(self._store_weight) @@ -1283,6 +1363,31 @@ def runs_photocurrent(self): ) return measurements + def merge(self, other, p=None): + new = super().merge(other, p) + new.collapse = self.collapse + other.collapse + new._time_dependent_weights = ( + self._time_dependent_weights or other._time_dependent_weights) + new._weights_absolute = ( + self._weights_absolute + other._weights_absolute) + + p_default = self._num_rel_trajectories / new._num_rel_trajectories + if p is None: + p = p_default + + for weight, isabs in zip(self.runs_weights, self._weights_absolute): + if isabs: + new.runs_weights.append(p * weight) + else: + new.runs_weights.append((p / p_default) * weight) + for weight, isabs in zip(other.runs_weights, other._weights_absolute): + if isabs: + new.runs_weights.append((1 - p) * weight) + else: + new.runs_weights.append(((1 - p) / (1 - p_default)) * weight) + + return new + class NmmcResult(McResult): """ @@ -1357,6 +1462,11 @@ def _add_trace(self, trajectory): self._sum_trace_rel += trajectory._total_weight_tlist self._sum2_trace_rel += np.abs(trajectory._total_weight_tlist) ** 2 + self._compute_avg_trace() + if self.options["keep_runs_results"]: + self.runs_trace.append(trajectory.trace) + + def _compute_avg_trace(self): avg = (self._sum_trace_abs + self._sum_trace_rel / self._num_rel_trajectories) avg2 = (self._sum2_trace_abs + @@ -1365,9 +1475,6 @@ def _add_trace(self, trajectory): self.average_trace = avg self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) - if self.options["keep_runs_results"]: - self.runs_trace.append(trajectory.trace) - @property def trace(self): """ @@ -1376,6 +1483,32 @@ def trace(self): """ return self.runs_trace or self.average_trace + def merge(self, other, p=None): + new = super().merge(other, p) + + p_default = self._num_rel_trajectories / new._num_rel_trajectories + if p is None: + p = p_default + + new._sum_trace_abs = (p * self._sum_trace_abs + + (1 - p) * other._sum_trace_abs) + new._sum2_trace_abs = (p * self._sum2_trace_abs + + (1 - p) * other._sum2_trace_abs) + new._sum_trace_rel = ( + (p / p_default) * self._sum_trace_rel + + ((1 - p) / (1 - p_default)) * other._sum_trace_abs + ) + new._sum2_trace_rel = ( + (p / p_default) * self._sum2_trace_rel + + ((1 - p) / (1 - p_default)) * other._sum2_trace_abs + ) + new._compute_avg_trace() + + if self.runs_trace and other.runs_trace: + new.runs_trace = self.runs_trace + other.runs_trace + + return new + def _to_dm(state): if state.type == "ket": diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index ee35c63f57..b08923f76f 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -165,6 +165,12 @@ def wiener_process(self): """ return self._trajectories_attr("wiener_process") + def merge(self, other, p=None): + raise NotImplementedError("Merging results of the stochastic solvers " + "is currently not supported. Please raise " + "an issue on GitHub if you would like to " + "see this feature.") + class _StochasticRHS(_MultiTrajRHS): """ From dc69fee10a5e4958649e997c3fee5057ca34e9bf Mon Sep 17 00:00:00 2001 From: Simon Cross Date: Mon, 1 Apr 2024 23:04:57 +0200 Subject: [PATCH 074/305] Update sphinx-rtd-theme to fix search. --- doc/requirements.txt | 2 +- doc/rtd-environment.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 6cb3ffacf5..26152dcc12 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -34,7 +34,7 @@ six==1.16.0 snowballstemmer==2.2.0 Sphinx==6.1.3 sphinx-gallery==0.12.2 -sphinx-rtd-theme==1.2.0 +sphinx-rtd-theme==1.2.1 sphinxcontrib-applehelp==1.0.3 sphinxcontrib-bibtex==2.5.0 sphinxcontrib-devhelp==1.0.2 diff --git a/doc/rtd-environment.yml b/doc/rtd-environment.yml index 7cafb0adc0..ff209b4a7e 100644 --- a/doc/rtd-environment.yml +++ b/doc/rtd-environment.yml @@ -38,7 +38,7 @@ dependencies: - snowballstemmer==2.2.0 - Sphinx==6.1.3 - sphinx-gallery==0.12.2 -- sphinx-rtd-theme==1.2.0 +- sphinx-rtd-theme==1.2.1 - sphinxcontrib-applehelp==1.0.4 - sphinxcontrib-bibtex==2.5.0 - sphinxcontrib-devhelp==1.0.2 From 32baebc883f922c8800b9a57f8d680e217ce122b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Mon, 1 Apr 2024 17:25:00 -0400 Subject: [PATCH 075/305] Apply suggestions from code review Co-authored-by: Simon Cross --- doc/guide/dynamics/dynamics-monte.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/guide/dynamics/dynamics-monte.rst b/doc/guide/dynamics/dynamics-monte.rst index 8724d94f9d..0115b9cd6a 100644 --- a/doc/guide/dynamics/dynamics-monte.rst +++ b/doc/guide/dynamics/dynamics-monte.rst @@ -445,7 +445,7 @@ Open Systems ``mcsolve`` can be used to study systems which have measurement and dissipative interactions with their environment. This is done by passing a Liouvillian including the dissipative interaction to the solver instead of a Hamiltonian. -In that cases the effective Liouvillian becomes: +In this case the effective Liouvillian becomes: .. math:: :label: Leff @@ -459,7 +459,7 @@ With the collapse probability becoming: \delta p =\delta t \sum_{n}\mathrm{tr}\left(\rho(t)C^{+}_{n}C_{n}\right), -And a jump with the collapse ``n`` changing the state as: +And a jump with the collapse operator ``n`` changing the state as: .. math:: :label: L_project From 8c684ec67a1ceb196541aaceedba237ec26332c3 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Tue, 2 Apr 2024 06:40:55 +0200 Subject: [PATCH 076/305] Fix broken links for the doc (#2376) --- CONTRIBUTING.md | 4 ++-- README.md | 8 ++++---- setup.cfg | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 104456854c..1d260bd44a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,6 @@ You are most welcome to contribute to QuTiP development by forking this repository and sending pull requests, or filing bug reports at the [issues page](https://github.com/qutip/qutip/issues). You can also help out with users' questions, or discuss proposed changes in the [QuTiP discussion group](https://groups.google.com/g/qutip). -All code contributions are acknowledged in the [contributors](https://qutip.org/docs/latest/contributors.html) section in the documentation. +All code contributions are acknowledged in the [contributors](https://qutip.readthedocs.io/en/stable/contributors.html) section in the documentation. -For more information, including technical advice, please see the ["contributing to QuTiP development" section of the documentation](https://qutip.org/docs/latest/development/contributing.html). +For more information, including technical advice, please see the ["contributing to QuTiP development" section of the documentation](https://qutip.readthedocs.io/en/stable/development/contributing.html). diff --git a/README.md b/README.md index ba6dbf7c6c..8af38f8465 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,10 @@ pip install qutip to get the minimal installation. You can instead use the target `qutip[full]` to install QuTiP with all its optional dependencies. -For more details, including instructions on how to build from source, see [the detailed installation guide in the documentation](https://qutip.org/docs/latest/installation.html). +For more details, including instructions on how to build from source, see [the detailed installation guide in the documentation](https://qutip.readthedocs.io/en/stable/installation.html). All back releases are also available for download in the [releases section of this repository](https://github.com/qutip/qutip/releases), where you can also find per-version changelogs. -For the most complete set of release notes and changelogs for historic versions, see the [changelog](https://qutip.org/docs/latest/changelog.html) section in the documentation. +For the most complete set of release notes and changelogs for historic versions, see the [changelog](https://qutip.readthedocs.io/en/stable/changelog.html) section in the documentation. The pre-release of QuTiP 5.0 is available on PyPI and can be installed using pip: @@ -107,9 +107,9 @@ Contribute You are most welcome to contribute to QuTiP development by forking this repository and sending pull requests, or filing bug reports at the [issues page](https://github.com/qutip/qutip/issues). You can also help out with users' questions, or discuss proposed changes in the [QuTiP discussion group](https://groups.google.com/g/qutip). -All code contributions are acknowledged in the [contributors](https://qutip.org/docs/latest/contributors.html) section in the documentation. +All code contributions are acknowledged in the [contributors](https://qutip.readthedocs.io/en/stable/contributors.html) section in the documentation. -For more information, including technical advice, please see the ["contributing to QuTiP development" section of the documentation](https://qutip.org/docs/latest/development/contributing.html). +For more information, including technical advice, please see the ["contributing to QuTiP development" section of the documentation](https://qutip.readthedocs.io/en/stable/development/contributing.html). Citing QuTiP diff --git a/setup.cfg b/setup.cfg index 7505274144..295ac3de55 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ license = BSD 3-Clause License license_files = LICENSE.txt project_urls = Bug Tracker = https://github.com/qutip/qutip/issues - Documentation = https://qutip.org/docs/latest/ + Documentation = https://qutip.readthedocs.io/en/stable/ Source Code = https://github.com/qutip/qutip classifiers = Development Status :: 2 - Pre-Alpha From 894ca112c1e90e1c770cf3c50bc8ee4e1e8b7bd2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 3 Apr 2024 06:01:59 +0900 Subject: [PATCH 077/305] Update readme note for v5 and setuptools requirement --- README.md | 13 ------------- qutip/core/coefficient.py | 2 +- setup.cfg | 1 + 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8af38f8465..9601a747be 100644 --- a/README.md +++ b/README.md @@ -22,19 +22,6 @@ and [J. R. Johansson](https://github.com/jrjohansson) [![PyPi Downloads](https://img.shields.io/pypi/dm/qutip?label=downloads%20%7C%20pip&logo=PyPI)](https://pypi.org/project/qutip) [![Conda-Forge Downloads](https://img.shields.io/conda/dn/conda-forge/qutip?label=downloads%20%7C%20conda&logo=Conda-Forge)](https://anaconda.org/conda-forge/qutip) -> **Note** -> -> The master branch now contains the alpha version of QuTiP 5. This is major -> revision that breaks compatibility in many small ways withh QuTiP 4.7. -> -> If you need to track QuTiP 4.7 changes or submit pull requests for 4.7, -> please use the `qutip-4.7.X` branch. -> -> If you need to track QuTiP 5 changes or submit pull request for 5, -> please use the `master` branch (and not the `dev.major` branch). -> -> The change to master happened on 16 January 2023 in commit @fccec5d. - QuTiP is open-source software for simulating the dynamics of closed and open quantum systems. It uses the excellent Numpy, Scipy, and Cython packages as numerical backends, and graphical output is provided by Matplotlib. QuTiP aims to provide user-friendly and efficient numerical simulations of a wide variety of quantum mechanical problems, including those with Hamiltonians and/or collapse operators with arbitrary time-dependence, commonly found in a wide range of physics applications. diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index cf30326685..11c87e2f57 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -11,8 +11,8 @@ import warnings import numbers from collections import defaultdict -from setuptools import setup, Extension try: + from setuptools import setup, Extension from Cython.Build import cythonize import filelock except ImportError: diff --git a/setup.cfg b/setup.cfg index 295ac3de55..1ea0a3bc71 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,7 @@ runtime_compilation = cython>=0.29.20; python_version>='3.10' cython>=0.29.20,<3.0.3; python_version<='3.9' filelock + setuptools semidefinite = cvxpy>=1.0 cvxopt From f643fdf8f03a6d4748faa2cd53d682ff2f743ec8 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 3 Apr 2024 13:27:36 +0900 Subject: [PATCH 078/305] Remove unnecessary fields --- qutip/solver/result.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index ae513a203d..edf192be04 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -712,8 +712,6 @@ def add(self, trajectory_info): trajectory.has_absolute_weight = False trajectory.has_time_dependent_weight = False trajectory.total_weight = 1 - trajectory._total_weight_tlist = np.ones_like(trajectory.times) - trajectory._final_weight = 1 for op in self._state_processors: op(trajectory) @@ -839,8 +837,7 @@ def average_final_state(self): Last states of each trajectories averaged into a density matrix. """ if ((self._sum_abs and not self._sum_abs.sum_final_state) or - (self._sum_rel and not self._sum_rel.sum_final_state)): - + (self._sum_rel and not self._sum_rel.sum_final_state)): if (average_states := self.average_states) is not None: return average_states[-1] return None @@ -1004,8 +1001,8 @@ def _merge_trajectories(self, other, p, p_default, p_is_default): # and there are no absolute weights present, we do not have to do any # extra work if (p_is_default and - self.num_trajectories == self._num_rel_trajectories and - other.num_trajectories == other._num_rel_trajectories): + self.num_trajectories == self._num_rel_trajectories and + other.num_trajectories == other._num_rel_trajectories): return self.trajectories + other.trajectories result = [] From d048fefa1bcf77a3432022a37574ce4ec21bca5e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 3 Apr 2024 15:04:20 +0900 Subject: [PATCH 079/305] Moved runs_weights to MultiTrajResult --- qutip/solver/result.py | 164 ++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 66 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index edf192be04..e0da726c3d 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -484,6 +484,10 @@ class MultiTrajResult(_BaseResult): The lists of expectation values returned are the *same* lists as those returned by ``.expect``. + runs_weights : list + For each trajectory, the weight with which that trajectory enters + averages. + solver : str or None The name of the solver generating these results. @@ -506,6 +510,7 @@ def __init__( self.trajectories = [] self.num_trajectories = 0 self.seeds = [] + self._weight_info = [] self.average_e_data = {} self.std_e_data = {} @@ -553,6 +558,10 @@ def _add_first_traj(self, trajectory): def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) + def _store_weight_info(self, trajectory): + self._weight_info.append( + (trajectory.total_weight, trajectory.has_absolute_weight)) + def _reduce_states(self, trajectory): if trajectory.has_absolute_weight: self._sum_abs.reduce_states(trajectory) @@ -674,6 +683,8 @@ def _post_init(self): store_trajectory = self.options["keep_runs_results"] if store_trajectory: self.add_processor(self._store_trajectory) + else: + self.add_processor(self._store_weight_info) if self._store_average_density_matrices: self.add_processor(self._reduce_states) if self._store_final_density_matrix: @@ -876,6 +887,19 @@ def expect(self): def e_data(self): return self.runs_e_data or self.average_e_data + @property + def runs_weights(self): + result = [] + if self._weight_info: + for w, isabs in self._weight_info: + result.append(w if isabs else w / self._num_rel_trajectories) + else: + for traj in self.trajectories: + w = traj.total_weight + isabs = traj.has_absolute_weight + result.append(w if isabs else w / self._num_rel_trajectories) + return result + def steady_state(self, N=0): """ Average the states of the last ``N`` times of every runs as a density @@ -966,22 +990,20 @@ def merge(self, other, p=None): other._num_rel_trajectories) new.seeds = self.seeds + other.seeds - p_default = self._num_rel_trajectories / new._num_rel_trajectories + p_equal = self._num_rel_trajectories / new._num_rel_trajectories if p is None: - p_is_default = True - p = p_default - else: - p_is_default = False + p = p_equal if self.trajectories and other.trajectories: - new.trajectories = self._merge_trajectories( - other, p, p_default, p_is_default) + new.trajectories = self._merge_trajectories(other, p, p_equal) + else: + new._weight_info = self._merge_weight_info(other, p, p_equal) new._sum_abs = _TrajectorySum.merge( self._sum_abs, p, other._sum_abs, 1 - p) new._sum_rel = _TrajectorySum.merge( - self._sum_rel, p / p_default, - other._sum_rel, (1 - p) / (1 - p_default)) + self._sum_rel, p / p_equal, + other._sum_rel, (1 - p) / (1 - p_equal)) new._create_e_data() @@ -995,32 +1017,67 @@ def merge(self, other, p=None): return new - def _merge_trajectories(self, other, p, p_default, p_is_default): - # Due to how the weights work, we may have to add weights to the - # trajectories of the merged result. Only if p has the default value - # and there are no absolute weights present, we do not have to do any - # extra work - if (p_is_default and + def _merge_weight(self, p, p_equal, isabs): + """ + Merging two result objects can make the trajectories pick up + merge weights. In order to have + rho_merge = p * rho1 + (1-p) * rho2, + the merge weights must be as defined here. The merge weight depends on + whether that trajectory has an absolute weight (`isabs`). The parameter + `p_equal` is the value of p where all trajectories contribute equally. + """ + if isabs: + return p + return p / p_equal + + def _merge_weight_info(self, other, p, p_equal): + new_weight_info = [] + + if self._weight_info: + for w, isabs in self._weight_info: + new_weight_info.append( + (w * self._merge_weight(p, p_equal, isabs), isabs) + ) + else: + for traj in self.trajectories: + w = traj.total_weight + isabs = traj.has_absolute_weight + new_weight_info.append( + (w * self._merge_weight(p, p_equal, isabs), isabs) + ) + + if other._weight_info: + for w, isabs in other._weight_info: + new_weight_info.append( + (w * self._merge_weight(1 - p, 1 - p_equal, isabs), isabs) + ) + else: + for traj in other.trajectories: + w = traj.total_weight + isabs = traj.has_absolute_weight + new_weight_info.append( + (w * self._merge_weight(1 - p, 1 - p_equal, isabs), isabs) + ) + + return new_weight_info + + def _merge_trajectories(self, other, p, p_equal): + if (p == p_equal and self.num_trajectories == self._num_rel_trajectories and other.num_trajectories == other._num_rel_trajectories): return self.trajectories + other.trajectories result = [] for traj in self.trajectories: - if traj.has_absolute_weight: + if (mweight := self._merge_weight(p, p_equal, traj.isabs)) != 1: traj = copy(traj) - traj.add_absolute_weight(p) - elif not p_is_default: - traj = copy(traj) - traj.add_relative_weight(p / p_default) + traj.add_relative_weight(mweight) result.append(traj) for traj in other.trajectories: - if traj.has_absolute_weight: - traj = copy(traj) - traj.add_absolute_weight(1 - p) - elif not p_is_default: + if (mweight := self._merge_weight( + 1 - p, 1 - p_equal, traj.isabs)) != 1: traj = copy(traj) - traj.add_relative_weight((1 - p) / (1 - p_default)) + traj.add_relative_weight(mweight) result.append(traj) return result @@ -1261,33 +1318,23 @@ class McResult(MultiTrajResult): Attributes ---------- collapse : list - For each runs, a list of every collapse as a tuple of the time it + For each run, a list of every collapse as a tuple of the time it happened and the corresponding ``c_ops`` index. """ # Collapse are only produced by mcsolve. def _add_collapse(self, trajectory): self.collapse.append(trajectory.collapse) - - # Need to store weights of trajectories to compute photocurrent - def _store_weight(self, trajectory): - self.runs_weights.append(trajectory.total_weight) - self._weights_absolute.append(trajectory.has_absolute_weight) if trajectory.has_time_dependent_weight: self._time_dependent_weights = True def _post_init(self): super()._post_init() self.num_c_ops = self.stats["num_collapse"] - + self._time_dependent_weights = False self.collapse = [] self.add_processor(self._add_collapse) - self.runs_weights = [] - self._weights_absolute = [] - self._time_dependent_weights = False - self.add_processor(self._store_weight) - @property def col_times(self): """ @@ -1332,7 +1379,6 @@ def photocurrent(self): mesurement = [ np.histogram(times, bins=tlist, weights=weights)[0] / np.diff(tlist) - / self.num_trajectories for times, weights in zip(collapse_times, collapse_weights) ] return mesurement @@ -1365,24 +1411,6 @@ def merge(self, other, p=None): new.collapse = self.collapse + other.collapse new._time_dependent_weights = ( self._time_dependent_weights or other._time_dependent_weights) - new._weights_absolute = ( - self._weights_absolute + other._weights_absolute) - - p_default = self._num_rel_trajectories / new._num_rel_trajectories - if p is None: - p = p_default - - for weight, isabs in zip(self.runs_weights, self._weights_absolute): - if isabs: - new.runs_weights.append(p * weight) - else: - new.runs_weights.append((p / p_default) * weight) - for weight, isabs in zip(other.runs_weights, other._weights_absolute): - if isabs: - new.runs_weights.append((1 - p) * weight) - else: - new.runs_weights.append(((1 - p) / (1 - p_default)) * weight) - return new @@ -1483,21 +1511,25 @@ def trace(self): def merge(self, other, p=None): new = super().merge(other, p) - p_default = self._num_rel_trajectories / new._num_rel_trajectories + p_eq = self._num_rel_trajectories / new._num_rel_trajectories if p is None: - p = p_default + p = p_eq - new._sum_trace_abs = (p * self._sum_trace_abs + - (1 - p) * other._sum_trace_abs) - new._sum2_trace_abs = (p * self._sum2_trace_abs + - (1 - p) * other._sum2_trace_abs) + new._sum_trace_abs = ( + self._merge_weight(p, p_eq, True) * self._sum_trace_abs + + self._merge_weight(1 - p, 1 - p_eq, True) * other._sum_trace_abs + ) + new._sum2_trace_abs = ( + self._merge_weight(p, p_eq, True) * self._sum2_trace_abs + + self._merge_weight(1 - p, 1 - p_eq, True) * other._sum2_trace_abs + ) new._sum_trace_rel = ( - (p / p_default) * self._sum_trace_rel + - ((1 - p) / (1 - p_default)) * other._sum_trace_abs + self._merge_weight(p, p_eq, False) * self._sum_trace_rel + + self._merge_weight(1 - p, 1 - p_eq, False) * other._sum_trace_rel ) new._sum2_trace_rel = ( - (p / p_default) * self._sum2_trace_rel + - ((1 - p) / (1 - p_default)) * other._sum2_trace_abs + self._merge_weight(p, p_eq, False) * self._sum2_trace_rel + + self._merge_weight(1 - p, 1 - p_eq, False) * other._sum2_trace_rel ) new._compute_avg_trace() From ba9071bc965105916e64bc26a683dac5d4ae6e73 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 3 Apr 2024 15:36:14 +0900 Subject: [PATCH 080/305] Updated target tolerance calculation for weighted traj --- qutip/solver/result.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index e0da726c3d..5f86454712 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -657,8 +657,7 @@ def _target_tolerance_end(self): Return the approximate number of trajectories needed to have this error within the tolerance fot all e_ops and times. """ - # TODO update this function for weighted trajectories - if self.num_trajectories <= 1: + if self._num_rel_trajectories <= 1: return np.inf avg, avg2 = self._average_computer() target = np.array( @@ -670,9 +669,9 @@ def _target_tolerance_end(self): target_ntraj = np.max((avg2 - abs(avg) ** 2) / target**2 + 1) self._estimated_ntraj = min(target_ntraj, self._target_ntraj) - if (self._estimated_ntraj - self.num_trajectories) <= 0: + if (self._estimated_ntraj - self._num_rel_trajectories) <= 0: self.stats["end_condition"] = "target tolerance reached" - return self._estimated_ntraj - self.num_trajectories + return self._estimated_ntraj - self._num_rel_trajectories def _post_init(self): self._target_ntraj = None From 95caad4beba45fc73a2097a583ad9a3d303f3fdb Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 3 Apr 2024 18:18:59 +0900 Subject: [PATCH 081/305] Started adding tests --- qutip/solver/result.py | 38 +++++++-- qutip/tests/solver/test_results.py | 131 +++++++++++++++++++++++++---- 2 files changed, 145 insertions(+), 24 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 5f86454712..919623fe33 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -666,7 +666,25 @@ def _target_tolerance_end(self): for mean, (atol, rtol) in zip(avg, self._target_tols) ] ) - target_ntraj = np.max((avg2 - abs(avg) ** 2) / target**2 + 1) + + one = np.array(1) + if self._num_rel_trajectories < self.num_trajectories: + # We only include traj. without abs. weights in this calculation. + # Since there are traj. with abs. weights., the weights don't add + # up to one. We have to consider that as follows: + # <(x - )^2> / <1> = / <1> - ^2 / <1>^2 + # and "<1>" is one minus the sum of all absolute weights + if self._weight_info: + for weight, isabs in self._weight_info: + if isabs: + one = one - weight + else: + for traj in self.trajectories: + if traj.has_absolute_weight: + one = one - traj.total_weight + + target_ntraj = np.max((avg2 / one - (abs(avg) ** 2) / (one ** 2)) / + target**2 + 1) self._estimated_ntraj = min(target_ntraj, self._target_ntraj) if (self._estimated_ntraj - self._num_rel_trajectories) <= 0: @@ -1473,10 +1491,10 @@ def _post_init(self): def _add_first_traj(self, trajectory): super()._add_first_traj(trajectory) - self._sum_trace_abs = np.zeros_like(trajectory.times) - self._sum_trace_rel = np.zeros_like(trajectory.times) - self._sum2_trace_abs = np.zeros_like(trajectory.times) - self._sum2_trace_rel = np.zeros_like(trajectory.times) + self._sum_trace_abs = np.zeros_like(trajectory.trace) + self._sum_trace_rel = np.zeros_like(trajectory.trace) + self._sum2_trace_abs = np.zeros_like(trajectory.trace) + self._sum2_trace_rel = np.zeros_like(trajectory.trace) def _add_trace(self, trajectory): if trajectory.has_absolute_weight: @@ -1491,10 +1509,12 @@ def _add_trace(self, trajectory): self.runs_trace.append(trajectory.trace) def _compute_avg_trace(self): - avg = (self._sum_trace_abs + - self._sum_trace_rel / self._num_rel_trajectories) - avg2 = (self._sum2_trace_abs + - self._sum2_trace_rel / self._num_rel_trajectories) + avg = self._sum_trace_abs + if self._num_rel_trajectories > 0: + avg = avg + self._sum_trace_rel / self._num_rel_trajectories + avg2 = self._sum2_trace_abs + if self._num_rel_trajectories > 0: + avg2 = avg2 + self._sum2_trace_rel / self._num_rel_trajectories self.average_trace = avg self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index a617149c08..28932dc5d6 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -3,7 +3,7 @@ import qutip from qutip.solver.result import ( - Result, MultiTrajResult, McResult, TrajectoryResult + Result, MultiTrajResult, McResult, NmmcResult, TrajectoryResult ) @@ -163,6 +163,42 @@ def test_repr_full(self): ">", ]) + def test_trajectory_result(self): + res = TrajectoryResult( + e_ops=qutip.num(5), + options=fill_options(store_states=True, store_final_state=True)) + for i in range(5): + res.add(i, qutip.basis(5, i)) + + assert not res.has_weight + assert not res.has_absolute_weight + assert not res.has_time_dependent_weight + assert res.total_weight == 1 + + res.add_absolute_weight(2) + res.add_absolute_weight(2) + assert res.has_weight and res.has_absolute_weight + assert not res.has_time_dependent_weight + assert res.total_weight == 4 + + res.add_relative_weight([1j ** i for i in range(5)]) + assert res.has_weight and res.has_absolute_weight + assert res.has_time_dependent_weight + np.testing.assert_array_equal(res.total_weight, + [4 * (1j ** i) for i in range(5)]) + + # weights do not modify states etc + assert res.states == [qutip.basis(5, i) for i in range(5)] + assert res.final_state == qutip.basis(5, 4) + np.testing.assert_array_equal(res.expect[0], range(5)) + + res = TrajectoryResult(e_ops=[], options=fill_options()) + res.add(0, qutip.fock_dm(2, 0)) + res.add_relative_weight(10) + assert res.has_weight + assert not (res.has_absolute_weight or res.has_time_dependent_weight) + assert res.total_weight == 10 + def e_op_num(t, state): """ An e_ops function that returns the ground state occupation. """ @@ -171,11 +207,16 @@ def e_op_num(t, state): class TestMultiTrajResult: def _fill_trajectories(self, multiresult, N, ntraj, - collapse=False, noise=0, dm=False): + collapse=False, noise=0, dm=False, + include_no_jump=False, rel_weights=None): + if rel_weights is None: + rel_weights = [None] * ntraj + # Fix the seed to avoid failing due to bad luck np.random.seed(1) - for _ in range(ntraj): - result = TrajectoryResult(multiresult._raw_ops, multiresult.options) + for k, w in enumerate(rel_weights): + result = TrajectoryResult(multiresult._raw_ops, + multiresult.options) result.collapse = [] for t in range(N): delta = 1 + noise * np.random.randn() @@ -187,14 +228,22 @@ def _fill_trajectories(self, multiresult, N, ntraj, result.collapse.append((t+0.1, 0)) result.collapse.append((t+0.2, 1)) result.collapse.append((t+0.3, 1)) + if include_no_jump and k == 0: + result.add_absolute_weight(0.25) + elif include_no_jump and k > 0: + result.add_relative_weight(0.75) + if w is not None: + result.add_relative_weight(w) + result.trace = w if multiresult.add((0, result)) <= 0: break - def _expect_check_types(self, multiresult): + def _check_types(self, multiresult): assert isinstance(multiresult.std_expect, list) assert isinstance(multiresult.average_e_data, dict) assert isinstance(multiresult.std_expect, list) assert isinstance(multiresult.average_e_data, dict) + assert isinstance(multiresult.runs_weights, list) if multiresult.trajectories: assert isinstance(multiresult.runs_expect, list) @@ -205,7 +254,8 @@ def _expect_check_types(self, multiresult): @pytest.mark.parametrize('keep_runs_results', [True, False]) @pytest.mark.parametrize('dm', [True, False]) - def test_McResult(self, dm, keep_runs_results): + @pytest.mark.parametrize('include_no_jump', [True, False]) + def test_McResult(self, dm, include_no_jump, keep_runs_results): N = 10 ntraj = 5 e_ops = [qutip.num(N), qutip.qeye(N)] @@ -213,11 +263,12 @@ def test_McResult(self, dm, keep_runs_results): m_res = McResult(e_ops, opt, stats={"num_collapse": 2}) m_res.add_end_condition(ntraj, None) - self._fill_trajectories(m_res, N, ntraj, collapse=True, dm=dm) + self._fill_trajectories(m_res, N, ntraj, collapse=True, + dm=dm, include_no_jump=include_no_jump) np.testing.assert_allclose(np.array(m_res.times), np.arange(N)) assert m_res.stats['end_condition'] == "ntraj reached" - self._expect_check_types(m_res) + self._check_types(m_res) assert np.all(np.array(m_res.col_which) < 2) assert isinstance(m_res.collapse, list) @@ -225,7 +276,50 @@ def test_McResult(self, dm, keep_runs_results): np.testing.assert_allclose(m_res.photocurrent[0], np.ones(N-1)) np.testing.assert_allclose(m_res.photocurrent[1], 2 * np.ones(N-1)) + @pytest.mark.parametrize(['include_no_jump', 'martingale', + 'result_trace', 'result_states'], [ + pytest.param(False, [[1.] * 10] * 5, [1.] * 10, + [qutip.fock_dm(10, i) for i in range(10)], + id='constant-martingale'), + pytest.param(True, [[1.] * 10] * 5, [1.] * 10, + [qutip.fock_dm(10, i) for i in range(10)], + id='constant-marting-no-jump'), + pytest.param(False, [[(j - 1) * np.sin(i) for i in range(10)] + for j in range(5)], + [np.sin(i) for i in range(10)], + [np.sin(i) * qutip.fock_dm(10, i) for i in range(10)], + id='timedep-marting'), + pytest.param(True, [[(j - 1) * np.sin(i) for i in range(10)] + for j in range(5)], + [(-0.25 + 1.5 * 0.75) * np.sin(i) for i in range(10)], + [(-0.25 + 1.5 * 0.75) * np.sin(i) * qutip.fock_dm(10, i) + for i in range(10)], + id='timedep-marting'), + ]) + def test_NmmcResult(self, include_no_jump, martingale, + result_trace, result_states): + N = 10 + ntraj = 5 + m_res = NmmcResult([], fill_options(), stats={"num_collapse": 2}) + m_res.add_end_condition(ntraj, None) + self._fill_trajectories(m_res, N, ntraj, collapse=True, + include_no_jump=include_no_jump, + rel_weights=martingale) + + np.testing.assert_allclose(np.array(m_res.times), np.arange(N)) + assert m_res.stats['end_condition'] == "ntraj reached" + self._check_types(m_res) + + assert np.all(np.array(m_res.col_which) < 2) + assert isinstance(m_res.collapse, list) + assert len(m_res.col_which[0]) == len(m_res.col_times[0]) + + np.testing.assert_almost_equal(m_res.average_trace, result_trace) + for s1, s2 in zip(m_res.average_states, result_states): + assert s1 == s2 + @pytest.mark.parametrize('keep_runs_results', [True, False]) + @pytest.mark.parametrize('include_no_jump', [True, False]) @pytest.mark.parametrize(["e_ops", "results"], [ pytest.param(qutip.num(5), [np.arange(5)], id="single-e-op"), pytest.param( @@ -241,12 +335,14 @@ def test_McResult(self, dm, keep_runs_results): id="list-e-ops", ), ]) - def test_multitraj_expect(self, keep_runs_results, e_ops, results): + def test_multitraj_expect(self, keep_runs_results, include_no_jump, + e_ops, results): N = 5 ntraj = 25 opt = fill_options(keep_runs_results=keep_runs_results) m_res = MultiTrajResult(e_ops, opt, stats={}) - self._fill_trajectories(m_res, N, ntraj, noise=0.01) + self._fill_trajectories(m_res, N, ntraj, noise=0.01, + include_no_jump=include_no_jump) for expect, expected in zip(m_res.average_expect, results): np.testing.assert_allclose(expect, expected, @@ -262,18 +358,20 @@ def test_multitraj_expect(self, keep_runs_results, e_ops, results): np.testing.assert_allclose(expect, expected, atol=1e-14, rtol=0.1) - self._expect_check_types(m_res) + self._check_types(m_res) assert m_res.stats['end_condition'] == "unknown" @pytest.mark.parametrize('keep_runs_results', [True, False]) + @pytest.mark.parametrize('include_no_jump', [True, False]) @pytest.mark.parametrize('dm', [True, False]) - def test_multitraj_state(self, keep_runs_results, dm): + def test_multitraj_state(self, keep_runs_results, include_no_jump, dm): N = 5 ntraj = 25 opt = fill_options(keep_runs_results=keep_runs_results) m_res = MultiTrajResult([], opt) - self._fill_trajectories(m_res, N, ntraj, dm=dm) + self._fill_trajectories(m_res, N, ntraj, dm=dm, + include_no_jump=include_no_jump) np.testing.assert_allclose(np.array(m_res.times), np.arange(N)) @@ -291,12 +389,14 @@ def test_multitraj_state(self, keep_runs_results, dm): assert m_res.runs_final_states[i] == expected @pytest.mark.parametrize('keep_runs_results', [True, False]) + @pytest.mark.parametrize('include_no_jump', [True, False]) @pytest.mark.parametrize('targettol', [ pytest.param(0.1, id='atol'), pytest.param([0.001, 0.1], id='rtol'), pytest.param([[0.001, 0.1], [0.1, 0]], id='tol_per_e_op'), ]) - def test_multitraj_targettol(self, keep_runs_results, targettol): + def test_multitraj_targettol(self, keep_runs_results, + include_no_jump, targettol): N = 10 ntraj = 1000 opt = fill_options( @@ -304,7 +404,8 @@ def test_multitraj_targettol(self, keep_runs_results, targettol): ) m_res = MultiTrajResult([qutip.num(N), qutip.qeye(N)], opt, stats={}) m_res.add_end_condition(ntraj, targettol) - self._fill_trajectories(m_res, N, ntraj, noise=0.1) + self._fill_trajectories(m_res, N, ntraj, noise=0.1, + include_no_jump=include_no_jump) assert m_res.stats['end_condition'] == "target tolerance reached" assert m_res.num_trajectories <= 1000 From f09cf77ec1f8fbadc692e37cce8173a84bc7847c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:22:57 +0000 Subject: [PATCH 082/305] Bump pillow from 10.2.0 to 10.3.0 in /doc Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.2.0 to 10.3.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/10.2.0...10.3.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 26152dcc12..62f907c792 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -21,7 +21,7 @@ packaging==23.0 parso==0.8.3 pexpect==4.8.0 pickleshare==0.7.5 -Pillow==10.2.0 +Pillow==10.3.0 prompt-toolkit==3.0.38 ptyprocess==0.7.0 Pygments==2.15.0 From 2517082e437cda3546a0210c0a594c652a7fed87 Mon Sep 17 00:00:00 2001 From: Maggie Date: Wed, 3 Apr 2024 21:11:07 -0400 Subject: [PATCH 083/305] fix end condition on mcsolve when using target tolerance --- qutip/solver/result.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 3e22f7fea0..630e837e01 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -657,7 +657,10 @@ def _target_tolerance_end(self): self._estimated_ntraj = min(target_ntraj, self._target_ntraj) if (self._estimated_ntraj - self.num_trajectories) <= 0: - self.stats["end_condition"] = "target tolerance reached" + if (self._estimated_ntraj - self._target_ntraj) < 0: + self.stats["end_condition"] = "target tolerance reached" + else: + self.stats["end_condition"] = "ntraj reached" return self._estimated_ntraj - self.num_trajectories def _post_init(self): From ebf8766ae2b7bd40872e27b542fc2742b8d5e5fb Mon Sep 17 00:00:00 2001 From: Maggie Date: Wed, 3 Apr 2024 21:49:21 -0400 Subject: [PATCH 084/305] add changelog file --- doc/changes/2382.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2382.bugfix diff --git a/doc/changes/2382.bugfix b/doc/changes/2382.bugfix new file mode 100644 index 0000000000..f923ff6b55 --- /dev/null +++ b/doc/changes/2382.bugfix @@ -0,0 +1 @@ +Ensure that end_condition of mcsolve result doesn't say target tolerance reached when it hasn't \ No newline at end of file From f4585e8c0f0a8b790bfbcab04ca4163925e45e1b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 4 Apr 2024 13:16:51 +0900 Subject: [PATCH 085/305] Added tests for merging multi trajectory results / mcresults / nmmcresults --- qutip/solver/result.py | 5 +- qutip/tests/solver/test_results.py | 116 +++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 919623fe33..5d28f2e33e 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1086,13 +1086,14 @@ def _merge_trajectories(self, other, p, p_equal): result = [] for traj in self.trajectories: - if (mweight := self._merge_weight(p, p_equal, traj.isabs)) != 1: + if (mweight := self._merge_weight( + p, p_equal, traj.has_absolute_weight)) != 1: traj = copy(traj) traj.add_relative_weight(mweight) result.append(traj) for traj in other.trajectories: if (mweight := self._merge_weight( - 1 - p, 1 - p_equal, traj.isabs)) != 1: + 1 - p, 1 - p_equal, traj.has_absolute_weight)) != 1: traj = copy(traj) traj.add_relative_weight(mweight) result.append(traj) diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index 28932dc5d6..abaf821763 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -446,6 +446,10 @@ def test_merge_result(self, keep_runs_results): merged_res = m_res1 + m_res2 assert merged_res.num_trajectories == 40 + assert len(merged_res.seeds) == 40 + assert len(merged_res.times) == 10 + assert len(merged_res.e_ops) == 1 + self._check_types(merged_res) np.testing.assert_allclose(merged_res.average_expect[0], np.arange(10), rtol=0.1) np.testing.assert_allclose( @@ -455,3 +459,115 @@ def test_merge_result(self, keep_runs_results): ) assert bool(merged_res.trajectories) == keep_runs_results assert merged_res.stats["run time"] == 3 + + @pytest.fixture(scope='session') + def fix_seed(self): + np.random.seed(1) + + def _random_ensemble(self, abs_weights=True, collapse=False, trace=False, + time_dep_weights=False, cls=MultiTrajResult): + dim = 10 + ntraj = 10 + tlist = [1, 2, 3] + + opt = fill_options( + keep_runs_results=False, store_states=True, store_final_state=True + ) + res = cls([qutip.num(dim)], opt, stats={"run time": 0, + "num_collapse": 2}) + + for _ in range(ntraj): + traj = TrajectoryResult(res._raw_ops, res.options) + seeds = np.random.randint(10_000, size=len(tlist)) + for t, seed in zip(tlist, seeds): + random_state = qutip.rand_ket(dim, seed=seed) + traj.add(t, random_state) + + if time_dep_weights and np.random.randint(2): + weights = np.random.rand(len(tlist)) + else: + weights = np.random.rand() + if abs_weights and np.random.randint(2): + traj.add_absolute_weight(weights) + else: + traj.add_relative_weight(weights) + + if collapse: + traj.collapse = [] + for _ in range(np.random.randint(5)): + traj.collapse.append( + (np.random.uniform(tlist[0], tlist[-1]), + np.random.randint(2))) + if trace: + traj.trace = np.random.rand(len(tlist)) + res.add((0, traj)) + + return res + + @pytest.mark.usefixtures('fix_seed') + @pytest.mark.parametrize('abs_weights1', [True, False]) + @pytest.mark.parametrize('abs_weights2', [True, False]) + @pytest.mark.parametrize('p', [0, 0.1, 1, None]) + def test_merge_weights(self, abs_weights1, abs_weights2, p): + ensemble1 = self._random_ensemble(abs_weights1) + ensemble2 = self._random_ensemble(abs_weights2) + merged = ensemble1.merge(ensemble2, p=p) + + if p is None: + p = ensemble1._num_rel_trajectories / ( + ensemble1._num_rel_trajectories + + ensemble2._num_rel_trajectories + ) + + np.testing.assert_almost_equal( + merged.expect[0], + p * ensemble1.expect[0] + (1 - p) * ensemble2.expect[0] + ) + + assert merged.final_state == ( + p * ensemble1.final_state + (1 - p) * ensemble2.final_state + ) + + for state1, state2, state in zip( + ensemble1.states, ensemble2.states, merged.states): + assert state == p * state1 + (1 - p) * state2 + + @pytest.mark.usefixtures('fix_seed') + @pytest.mark.parametrize('p', [0, 0.1, 1, None]) + def test_merge_mcresult(self, p): + ensemble1 = self._random_ensemble(collapse=True, + time_dep_weights=False, cls=McResult) + ensemble2 = self._random_ensemble(collapse=True, + time_dep_weights=False, cls=McResult) + merged = ensemble1.merge(ensemble2, p=p) + + if p is None: + p = ensemble1._num_rel_trajectories / ( + ensemble1._num_rel_trajectories + + ensemble2._num_rel_trajectories + ) + + assert merged.num_trajectories == len(merged.collapse) + + for c1, c2, c in zip(ensemble1.photocurrent, + ensemble2.photocurrent, + merged.photocurrent): + np.testing.assert_almost_equal(c, p * c1 + (1 - p) * c2) + + @pytest.mark.usefixtures('fix_seed') + @pytest.mark.parametrize('p', [0, 0.1, 1, None]) + def test_merge_nmmcresult(self, p): + ensemble1 = self._random_ensemble( + collapse=True, trace=True, time_dep_weights=True, cls=NmmcResult) + ensemble2 = self._random_ensemble( + collapse=True, trace=True, time_dep_weights=True, cls=NmmcResult) + merged = ensemble1.merge(ensemble2, p=p) + + if p is None: + p = ensemble1._num_rel_trajectories / ( + ensemble1._num_rel_trajectories + + ensemble2._num_rel_trajectories + ) + + np.testing.assert_almost_equal( + merged.trace, p * ensemble1.trace + (1 - p) * ensemble2.trace) From e60e28375555101ea8357426de8215349420ae52 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 4 Apr 2024 13:30:34 +0900 Subject: [PATCH 086/305] Added improved sampling to nmmcsolve tests --- qutip/tests/solver/test_nm_mcsolve.py | 64 ++++++++++++++++----------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index 1dc64b2968..3bfa9ca990 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -7,7 +7,8 @@ from qutip.solver.nm_mcsolve import nm_mcsolve, NonMarkovianMCSolver -def test_agreement_with_mesolve_for_negative_rates(): +@pytest.mark.parametrize("improved_sampling", [True, False]) +def test_agreement_with_mesolve_for_negative_rates(improved_sampling): """ A rough test that nm_mcsolve agress with mesolve in the presence of negative rates. @@ -38,8 +39,8 @@ def test_agreement_with_mesolve_for_negative_rates(): ] mc_result = nm_mcsolve( H, psi0, times, ops_and_rates, - args=args, e_ops=e_ops, ntraj=2000, - options={"rtol": 1e-8}, + args=args, e_ops=e_ops, ntraj=1000 if improved_sampling else 2000, + options={"rtol": 1e-8, "improved_sampling": improved_sampling}, seeds=0, ) @@ -173,10 +174,13 @@ def _assert_expect(self, result, expected, tol): for test, expected_part in zip(result.expect, expected): np.testing.assert_allclose(test, expected_part, rtol=tol) + @pytest.mark.parametrize("improved_sampling", [True, False]) def test_states_and_expect( - self, hamiltonian, args, ops_and_rates, expected, tol + self, hamiltonian, args, ops_and_rates, expected, tol, + improved_sampling ): - options = {"store_states": True, "map": "serial"} + options = {"store_states": True, "map": "serial", + "improved_sampling": improved_sampling} result = nm_mcsolve( hamiltonian, self.state, self.times, args=args, ops_and_rates=ops_and_rates, @@ -221,10 +225,11 @@ def pytest_generate_tests(self, metafunc): # runtimes shorter. The known-good cases are still tested in the other # test cases, this is just testing the single-output behaviour. - def test_states_only( - self, hamiltonian, args, ops_and_rates, expected, tol - ): - options = {"store_states": True, "map": "serial"} + @pytest.mark.parametrize("improved_sampling", [True, False]) + def test_states_only(self, hamiltonian, args, ops_and_rates, expected, tol, + improved_sampling): + options = {"store_states": True, "map": "serial", + "improved_sampling": improved_sampling} result = nm_mcsolve( hamiltonian, self.state, self.times, args=args, ops_and_rates=ops_and_rates, @@ -232,13 +237,14 @@ def test_states_only( ) self._assert_states(result, expected, tol) - def test_expect_only( - self, hamiltonian, args, ops_and_rates, expected, tol - ): + @pytest.mark.parametrize("improved_sampling", [True, False]) + def test_expect_only(self, hamiltonian, args, ops_and_rates, expected, tol, + improved_sampling): + options = {'map': 'serial', "improved_sampling": improved_sampling} result = nm_mcsolve( hamiltonian, self.state, self.times, args=args, ops_and_rates=ops_and_rates, - e_ops=self.e_ops, ntraj=self.ntraj, options={'map': 'serial'}, + e_ops=self.e_ops, ntraj=self.ntraj, options=options, ) self._assert_expect(result, expected, tol) @@ -324,8 +330,9 @@ def test_stored_collapse_operators_and_times(): assert all(col in [0, 1] for col in result.col_which[0]) -@pytest.mark.parametrize('keep_runs_results', [True, False]) -def test_states_outputs(keep_runs_results): +@pytest.mark.parametrize("improved_sampling", [True, False]) +@pytest.mark.parametrize("keep_runs_results", [True, False]) +def test_states_outputs(keep_runs_results, improved_sampling): # We're just testing the output value, so it's important whether certain # things are complex or real, but not what the magnitudes of constants are. focks = 5 @@ -348,8 +355,7 @@ def test_states_outputs(keep_runs_results): options={ "keep_runs_results": keep_runs_results, "map": "serial", - }, - ) + "improved_sampling": improved_sampling}) assert len(data.average_states) == len(times) assert isinstance(data.average_states[0], qutip.Qobj) @@ -385,8 +391,9 @@ def test_states_outputs(keep_runs_results): assert data.stats['end_condition'] == "ntraj reached" -@pytest.mark.parametrize('keep_runs_results', [True, False]) -def test_expectation_outputs(keep_runs_results): +@pytest.mark.parametrize("improved_sampling", [True, False]) +@pytest.mark.parametrize("keep_runs_results", [True, False]) +def test_expectation_outputs(keep_runs_results, improved_sampling): # We're just testing the output value, so it's important whether certain # things are complex or real, but not what the magnitudes of constants are. focks = 5 @@ -410,8 +417,7 @@ def test_expectation_outputs(keep_runs_results): options={ "keep_runs_results": keep_runs_results, "map": "serial", - }, - ) + "improved_sampling": improved_sampling}) assert isinstance(data.average_expect[0][1], float) assert isinstance(data.average_expect[1][1], float) assert isinstance(data.average_expect[2][1], complex) @@ -522,7 +528,8 @@ def test_stepping(self): assert state_1 == state_2 -def test_timeout(): +@pytest.mark.parametrize("improved_sampling", [True, False]) +def test_timeout(improved_sampling): size = 10 ntraj = 1000 a = qutip.destroy(size) @@ -537,12 +544,14 @@ def test_timeout(): e_ops = [qutip.num(size)] res = nm_mcsolve( H, state, times, ops_and_rates, e_ops, ntraj=ntraj, - options={'map': 'serial'}, timeout=1e-6, + options={'map': 'serial', "improved_sampling": improved_sampling}, + timeout=1e-6, ) assert res.stats['end_condition'] == 'timeout' -def test_super_H(): +@pytest.mark.parametrize("improved_sampling", [True, False]) +def test_super_H(improved_sampling): size = 10 ntraj = 1000 a = qutip.destroy(size) @@ -558,12 +567,13 @@ def test_super_H(): e_ops = [qutip.num(size)] mc_expected = nm_mcsolve( H, state, times, ops_and_rates, e_ops, ntraj=ntraj, - target_tol=0.1, options={'map': 'serial'}, + target_tol=0.1, options={'map': 'serial', + "improved_sampling": improved_sampling}, ) mc = nm_mcsolve( qutip.liouvillian(H), state, times, ops_and_rates, e_ops, ntraj=ntraj, - target_tol=0.1, options={'map': 'serial'}, - ) + target_tol=0.1, options={'map': 'serial', + "improved_sampling": improved_sampling}) np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.5) From 283e06094f4dc80aef569bfae6393e004549bd83 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 4 Apr 2024 13:30:45 +0900 Subject: [PATCH 087/305] Fix typo in mcsolve test --- qutip/tests/solver/test_mcsolve.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 717fc74fc1..965ab781c7 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -406,7 +406,9 @@ def test_super_H(improved_sampling): c_ops = np.sqrt(coupling * (n_th + 1)) * a e_ops = [qutip.num(size)] mc_expected = mcsolve(H, state, times, c_ops, e_ops, ntraj=ntraj, - target_tol=0.1, options={'map': 'serial'}) + target_tol=0.1, + options={'map': 'serial', + "improved_sampling": improved_sampling}) mc = mcsolve(qutip.liouvillian(H), state, times, c_ops, e_ops, ntraj=ntraj, target_tol=0.1, options={'map': 'serial', From 5fabdd307f6a89b81f7172622a01b0efff61e5b3 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 4 Apr 2024 18:29:16 +0900 Subject: [PATCH 088/305] Performance improvement --- qutip/solver/result.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 5d28f2e33e..154d4fc7c2 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -510,7 +510,6 @@ def __init__( self.trajectories = [] self.num_trajectories = 0 self.seeds = [] - self._weight_info = [] self.average_e_data = {} self.std_e_data = {} @@ -530,6 +529,10 @@ def __init__( self._sum_abs = None # Number of trajectories without specified absolute weight self._num_rel_trajectories = 0 + # Needed for merging results + self._weight_info = [] + # Needed for target tolerance computation + self._total_abs_weight = np.array(0) self._post_init(**kw) @@ -559,8 +562,13 @@ def _store_trajectory(self, trajectory): self.trajectories.append(trajectory) def _store_weight_info(self, trajectory): - self._weight_info.append( - (trajectory.total_weight, trajectory.has_absolute_weight)) + if trajectory.has_absolute_weight: + self._total_abs_weight = ( + self._total_abs_weight + trajectory.total_weight) + if len(self.trajectories) == 0: + # store weight info only if trajectories are not stored + self._weight_info.append( + (trajectory.total_weight, trajectory.has_absolute_weight)) def _reduce_states(self, trajectory): if trajectory.has_absolute_weight: @@ -674,14 +682,7 @@ def _target_tolerance_end(self): # up to one. We have to consider that as follows: # <(x - )^2> / <1> = / <1> - ^2 / <1>^2 # and "<1>" is one minus the sum of all absolute weights - if self._weight_info: - for weight, isabs in self._weight_info: - if isabs: - one = one - weight - else: - for traj in self.trajectories: - if traj.has_absolute_weight: - one = one - traj.total_weight + one = one - self._total_abs_weight target_ntraj = np.max((avg2 / one - (abs(avg) ** 2) / (one ** 2)) / target**2 + 1) @@ -700,14 +701,13 @@ def _post_init(self): store_trajectory = self.options["keep_runs_results"] if store_trajectory: self.add_processor(self._store_trajectory) - else: - self.add_processor(self._store_weight_info) if self._store_average_density_matrices: self.add_processor(self._reduce_states) if self._store_final_density_matrix: self.add_processor(self._reduce_final_state) if self._raw_ops: self.add_processor(self._reduce_expect) + self.add_processor(self._store_weight_info) self.stats["end_condition"] = "unknown" From 8eb3c988d884e308c9ebdca38df9a521aeae5b13 Mon Sep 17 00:00:00 2001 From: Maggie Date: Thu, 4 Apr 2024 14:24:19 -0400 Subject: [PATCH 089/305] add test --- qutip/tests/solver/test_mcsolve.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 717fc74fc1..05d4ca520a 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -391,6 +391,28 @@ def test_timeout(improved_sampling): timeout=1e-6) assert res.stats['end_condition'] == 'timeout' +@pytest.mark.parametrize("improved_sampling", [True, False]) +def test_target_tol(improved_sampling): + size = 10 + ntraj = 100 + a = qutip.destroy(size) + H = qutip.num(size) + state = qutip.basis(size, size-1) + times = np.linspace(0, 1.0, 100) + coupling = 0.5 + n_th = 0.05 + c_ops = np.sqrt(coupling * (n_th + 1)) * a + e_ops = [qutip.num(size)] + + options = {'map': 'serial', "improved_sampling": improved_sampling} + + res = mcsolve(H, state, times, c_ops, e_ops, ntraj=ntraj, options=options, + target_tol = 0.5) + assert res.stats['end_condition'] == 'target tolerance reached' + + res = mcsolve(H, state, times, c_ops, e_ops, ntraj=ntraj, options=options, + target_tol = 1e-6) + assert res.stats['end_condition'] == 'ntraj reached' @pytest.mark.parametrize("improved_sampling", [True, False]) def test_super_H(improved_sampling): From 31c0b81db1e00174b714f0fc96d0d530d5f4f640 Mon Sep 17 00:00:00 2001 From: Boxi Li Date: Fri, 5 Apr 2024 04:41:59 +0200 Subject: [PATCH 090/305] Use CSR as the default for expand_operator (#2380) Use CSR as the default for expand_operator because the output is usually sparse. --- doc/changes/2280.bugfix | 1 + qutip/core/tensor.py | 11 +++++++++-- qutip/tests/core/test_tensor.py | 10 ++++++++++ 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 doc/changes/2280.bugfix diff --git a/doc/changes/2280.bugfix b/doc/changes/2280.bugfix new file mode 100644 index 0000000000..d8f77bbd21 --- /dev/null +++ b/doc/changes/2280.bugfix @@ -0,0 +1 @@ +Use CSR as the default for expand_operator \ No newline at end of file diff --git a/qutip/core/tensor.py b/qutip/core/tensor.py index b9fde85fb9..b17e06d01c 100644 --- a/qutip/core/tensor.py +++ b/qutip/core/tensor.py @@ -17,6 +17,7 @@ dims_idxs_to_tensor_idxs ) from . import data as _data +from .. import settings class _reverse_partial_tensor: @@ -413,7 +414,7 @@ def _targets_to_list(targets, oper=None, N=None): return targets -def expand_operator(oper, dims, targets): +def expand_operator(oper, dims, targets, dtype=None): """ Expand an operator to one that acts on a system with desired dimensions. e.g. @@ -435,13 +436,19 @@ def expand_operator(oper, dims, targets): E.g ``[2, 3, 2, 3, 4]``. targets : int or list of int The indices of subspace that are acted on. + dtype : str, optional + Data type of the output :class:`.Qobj`. By default it uses the data + type specified in settings. If no data type is specified + in settings it uses the ``CSR`` data type. Returns ------- expanded_oper : :class:`.Qobj` - The expanded operator acting on a system with desired dimension. + The expanded operator acting on a system with the desired dimension. """ from .operators import identity + dtype = dtype or settings.core["default_dtype"] or _data.CSR + oper = oper.to(dtype) N = len(dims) targets = _targets_to_list(targets, oper=oper, N=N) _check_oper_dims(oper, dims=dims, targets=targets) diff --git a/qutip/tests/core/test_tensor.py b/qutip/tests/core/test_tensor.py index 10e493e46f..08556bf66e 100644 --- a/qutip/tests/core/test_tensor.py +++ b/qutip/tests/core/test_tensor.py @@ -255,3 +255,13 @@ def test_non_qubit_systems(self, dimensions): test = expand_operator(base_test, dims=dimensions, targets=targets) assert test.dims == expected.dims np.testing.assert_allclose(test.full(), expected.full()) + + def test_dtype(self): + expanded_qobj = expand_operator( + qutip.gates.cnot(), dims=[2, 2, 2], targets=[0, 1] + ).data + assert isinstance(expanded_qobj, qutip.data.CSR) + expanded_qobj = expand_operator( + qutip.gates.cnot(), dims=[2, 2, 2], targets=[0, 1], dtype="dense" + ).data + assert isinstance(expanded_qobj, qutip.data.Dense) From 09fb520b9b1b8aaa289f8796387a45a7d85fadb9 Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Sun, 7 Apr 2024 17:13:20 +0530 Subject: [PATCH 091/305] Fixed negatitivity function --- qutip/entropy.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutip/entropy.py b/qutip/entropy.py index 06282c4de7..3c329dde10 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -4,8 +4,9 @@ from numpy import conj, e, inf, imag, inner, real, sort, sqrt from numpy.lib.scimath import log, log2 -from qutip import partial_transpose, ket2dm -from . import (ptrace, tensor, sigmay, +from numpy import np +from .partial_transpose import partial_transpose +from . import (ptrace, tensor, sigmay, ket2dm, expand_operator) from .core import data as _data @@ -130,7 +131,7 @@ def negativity(rho, subsys, method='tracenorm', logarithmic=False): Experimental. """ - if rho.isket: + if rho.isket or rho.isbra: rho = ket2dm(rho) mask = [idx == subsys for idx, n in enumerate(rho.dims[0])] rho_pt = partial_transpose(rho, mask) From 144f9e7fafbb1349aeb988bae14a6c69b4454f7a Mon Sep 17 00:00:00 2001 From: Neill Lambert Date: Mon, 8 Apr 2024 14:29:13 +0900 Subject: [PATCH 092/305] ENR state fixes for steadystate solvers Updated steadystate solvers to use _dims instead of dims to support ENR states --- qutip/solver/steadystate.py | 12 +++++---- qutip/tests/test_enr_state_operator.py | 37 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py index 5993822bd7..c484d782ae 100644 --- a/qutip/solver/steadystate.py +++ b/qutip/solver/steadystate.py @@ -233,7 +233,6 @@ def _steadystate_direct(A, weight, **kw): else: warn("Only sparse solver use preconditioners.", RuntimeWarning) - method = kw.pop("method", None) steadystate = _data.solve(L, b, method, options=kw) @@ -243,7 +242,7 @@ def _steadystate_direct(A, weight, **kw): rho_ss = _data.column_unstack(steadystate, n) rho_ss = _data.add(rho_ss, rho_ss.adjoint()) * 0.5 - return Qobj(rho_ss, dims=A.dims[0], isherm=True) + return Qobj(rho_ss, dims=A._dims[0].oper, isherm=True) def _steadystate_eigen(L, **kw): @@ -258,9 +257,12 @@ def _steadystate_eigen(L, **kw): def _steadystate_svd(L, **kw): + N = L.shape[0] + n = int(N**0.5) u, s, vh = _data.svd(L.data, True) - vec = Qobj(_data.split_columns(vh.adjoint())[-1], dims=[L.dims[0],[1]]) - rho = vector_to_operator(vec) + vec = _data.split_columns(vh.adjoint())[-1] + rho = _data.column_unstack(vec, n) + rho = Qobj(rho, dims=L._dims[0].oper, isherm=True) return rho / rho.tr() @@ -305,7 +307,7 @@ def _steadystate_power(A, **kw): if use_rcm: y = _reverse_rcm(y, perm) - rho_ss = Qobj(_data.column_unstack(y, N**0.5), dims=A.dims[0]) + rho_ss = Qobj(_data.column_unstack(y, N**0.5), dims=A._dims[0].oper) rho_ss = rho_ss + rho_ss.dag() rho_ss = rho_ss / rho_ss.tr() rho_ss.isherm = True diff --git a/qutip/tests/test_enr_state_operator.py b/qutip/tests/test_enr_state_operator.py index d5949eaea7..9257e3266e 100644 --- a/qutip/tests/test_enr_state_operator.py +++ b/qutip/tests/test_enr_state_operator.py @@ -152,3 +152,40 @@ def test_mesolve_ENR(): np.testing.assert_allclose(result_JC.expect[0], result_enr.expect[0], atol=1e-2) + + +def test_steadystate_ENR(): + # Ensure ENR states work with steadystate functions + # We compare the output to an exact truncation of the + # single-excitation Jaynes-Cummings model + eps = 2 * np.pi + omega_c = 2 * np.pi + g = 0.1 * omega_c + gam = 0.01 * omega_c + N_cut = 2 + + sz = qutip.sigmaz() & qutip.qeye(N_cut) + sm = qutip.destroy(2).dag() & qutip.qeye(N_cut) + a = qutip.qeye(2) & qutip.destroy(N_cut) + H_JC = (0.5 * eps * sz + omega_c * a.dag()*a + + g * (a * sm.dag() + a.dag() * sm)) + c_ops = [np.sqrt(gam) * a] + + result_JC = qutip.steadystate(H_JC, c_ops) + exp_sz_JC = qutip.expect(sz, result_JC) + + N_exc = 1 + dims = [2, N_cut] + d = qutip.enr_destroy(dims, N_exc) + sz = 2*d[0].dag()*d[0]-1 + b = d[0] + a = d[1] + H_enr = (eps * b.dag()*b + omega_c * a.dag() * a + + g * (b.dag() * a + a.dag() * b)) + c_ops = [np.sqrt(gam) * a] + + result_enr = qutip.steadystate(H_enr, c_ops) + exp_sz_enr = qutip.expect(sz, result_enr) + + np.testing.assert_allclose(exp_sz_JC, + exp_sz_enr, atol=1e-2) From c30f496d6815da22e062a7f2f1cf56521b7de6d8 Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Tue, 9 Apr 2024 23:07:48 +0530 Subject: [PATCH 093/305] remove numpy --- qutip/entropy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutip/entropy.py b/qutip/entropy.py index 3c329dde10..26f3ae89ef 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -4,7 +4,6 @@ from numpy import conj, e, inf, imag, inner, real, sort, sqrt from numpy.lib.scimath import log, log2 -from numpy import np from .partial_transpose import partial_transpose from . import (ptrace, tensor, sigmay, ket2dm, expand_operator) From 09807ed59415f9cf045d797c7d13207deabe5cd4 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 9 Apr 2024 14:23:24 -0400 Subject: [PATCH 094/305] gates use default_dtype and set isherm --- qutip/core/gates.py | 172 ++++++++++++++++++++++----------- qutip/core/operators.py | 56 +++++++---- qutip/core/qobj.py | 9 +- qutip/tests/core/test_gates.py | 58 ++++++++++- 4 files changed, 214 insertions(+), 81 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index ee97c819f6..e28e53173f 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -5,6 +5,9 @@ import numpy as np import scipy.sparse as sp from . import Qobj, qeye, sigmax, fock_dm, qdiags, qeye_like +from .dimensions import Dimensions +from .. import settings +from . import data as _data __all__ = [ @@ -35,10 +38,11 @@ "toffoli", "hadamard_transform", "qubit_clifford_group", + "globalphase", ] -def cy_gate(*, dtype="csr"): +def cy_gate(*, dtype=None): """Controlled Y gate. Parameters @@ -52,13 +56,15 @@ def cy_gate(*, dtype="csr"): result : :class:`.Qobj` Quantum object for operator describing the rotation. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=True, ).to(dtype) -def cz_gate(*, dtype="csr"): +def cz_gate(*, dtype=None): """Controlled Z gate. Parameters @@ -72,10 +78,11 @@ def cz_gate(*, dtype="csr"): result : :class:`.Qobj` Quantum object for operator describing the rotation. """ - return qdiags([1, 1, 1, -1], dims=[[2, 2], [2, 2]], dtype=dtype) + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return qdiags([1, 1, 1, -1], dims=dims_2_qb, dtype=dtype) -def s_gate(*, dtype="csr"): +def s_gate(*, dtype=None): """Single-qubit rotation also called Phase gate or the Z90 gate. Parameters @@ -91,10 +98,11 @@ def s_gate(*, dtype="csr"): a 90 degree rotation around the z-axis. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return qdiags([1, 1j], dtype=dtype) -def cs_gate(*, dtype="csr"): +def cs_gate(*, dtype=None): """Controlled S gate. Parameters @@ -109,10 +117,11 @@ def cs_gate(*, dtype="csr"): Quantum object for operator describing the rotation. """ - return qdiags([1, 1, 1, 1j], dims=[[2, 2], [2, 2]], dtype=dtype) + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return qdiags([1, 1, 1, 1j], dims=dims_2_qb, dtype=dtype) -def t_gate(*, dtype="csr"): +def t_gate(*, dtype=None): """Single-qubit rotation related to the S gate by the relationship S=T*T. Parameters @@ -127,10 +136,11 @@ def t_gate(*, dtype="csr"): Quantum object for operator describing a phase shift of pi/4. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return qdiags([1, np.exp(1j * np.pi / 4)], dtype=dtype) -def ct_gate(*, dtype="csr"): +def ct_gate(*, dtype=None): """Controlled T gate. Parameters @@ -145,18 +155,22 @@ def ct_gate(*, dtype="csr"): Quantum object for operator describing the rotation. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return qdiags( [1, 1, 1, np.exp(1j * np.pi / 4)], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, dtype=dtype, ) -def rx(phi, *, dtype="dense"): +def rx(phi, *, dtype=None): """Single-qubit rotation for operator sigmax with angle phi. Parameters ---------- + phi : float + Rotation angle + dtype : str or type, [keyword only] [optional] Storage representation. Any data-layer known to `qutip.data.to` is accepted. @@ -167,19 +181,24 @@ def rx(phi, *, dtype="dense"): Quantum object for operator describing the rotation. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense return Qobj( [ [np.cos(phi / 2), -1j * np.sin(phi / 2)], [-1j * np.sin(phi / 2), np.cos(phi / 2)], - ] + ], + isherm=(phi % (2 * np.pi) <= settings.core["atol"]), ).to(dtype) -def ry(phi, *, dtype="dense"): +def ry(phi, *, dtype=None): """Single-qubit rotation for operator sigmay with angle phi. Parameters ---------- + phi : float + Rotation angle + dtype : str or type, [keyword only] [optional] Storage representation. Any data-layer known to `qutip.data.to` is accepted. @@ -190,19 +209,24 @@ def ry(phi, *, dtype="dense"): Quantum object for operator describing the rotation. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense return Qobj( [ [np.cos(phi / 2), -np.sin(phi / 2)], [np.sin(phi / 2), np.cos(phi / 2)], - ] + ], + isherm=(phi % (2 * np.pi) <= settings.core["atol"]), ).to(dtype) -def rz(phi, *, dtype="csr"): +def rz(phi, *, dtype=None): """Single-qubit rotation for operator sigmaz with angle phi. Parameters ---------- + phi : float + Rotation angle + dtype : str or type, [keyword only] [optional] Storage representation. Any data-layer known to `qutip.data.to` is accepted. @@ -213,10 +237,11 @@ def rz(phi, *, dtype="csr"): Quantum object for operator describing the rotation. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return qdiags([np.exp(-1j * phi / 2), np.exp(1j * phi / 2)], dtype=dtype) -def sqrtnot(*, dtype="dense"): +def sqrtnot(*, dtype=None): """Single-qubit square root NOT gate. Parameters @@ -231,10 +256,13 @@ def sqrtnot(*, dtype="dense"): Quantum object for operator describing the square root NOT gate. """ - return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]]).to(dtype) + dtype = dtype or settings.core["default_dtype"] or _data.Dense + return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]], isherm=False).to( + dtype + ) -def snot(*, dtype="dense"): +def snot(*, dtype=None): """Quantum object representing the SNOT (Hadamard) gate. Parameters @@ -249,10 +277,11 @@ def snot(*, dtype="dense"): Quantum object representation of SNOT gate. """ - return Qobj([[1, 1], [1, -1]]).to(dtype) / np.sqrt(2.0) + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return Qobj([[1, 1], [1, -1]], isherm=True).to(dtype) / np.sqrt(2.0) -def phasegate(theta, *, dtype="csr"): +def phasegate(theta, *, dtype=None): """ Returns quantum object representing the phase shift gate. @@ -270,10 +299,11 @@ def phasegate(theta, *, dtype="csr"): Quantum object representation of phase shift gate. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return qdiags([1, np.exp(1.0j * theta)], dtype=dtype) -def qrot(theta, phi, *, dtype="dense"): +def qrot(theta, phi, *, dtype=None): """ Single qubit rotation driving by Rabi oscillation with 0 detune. @@ -293,11 +323,13 @@ def qrot(theta, phi, *, dtype="dense"): Quantum object representation of physical qubit rotation under a rabi pulse. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense return Qobj( [ [np.cos(theta / 2), -1j * np.exp(-1j * phi) * np.sin(theta / 2)], [-1j * np.exp(1j * phi) * np.sin(theta / 2), np.cos(theta / 2)], - ] + ], + isherm=(theta % (2 * np.pi) <= settings.core["atol"]), ).to(dtype) @@ -306,7 +338,10 @@ def qrot(theta, phi, *, dtype="dense"): # -def cphase(theta, *, dtype="csr"): +dims_2_qb = Dimensions([[2, 2], [2, 2]]) + + +def cphase(theta, *, dtype=None): """ Returns quantum object representing the controlled phase shift gate. @@ -323,12 +358,11 @@ def cphase(theta, *, dtype="csr"): U : qobj Quantum object representation of controlled phase gate. """ - return qdiags( - [1, 1, 1, np.exp(1.0j * theta)], dims=[[2, 2], [2, 2]], dtype=dtype - ) + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return qdiags([1, 1, 1, np.exp(1.0j * theta)], dims=dims_2_qb, dtype=dtype) -def cnot(*, dtype="csr"): +def cnot(*, dtype=None): """ Quantum object representing the CNOT gate. @@ -344,13 +378,15 @@ def cnot(*, dtype="csr"): Quantum object representation of CNOT gate """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=True, ).to(dtype) -def csign(*, dtype="csr"): +def csign(*, dtype=None): """ Quantum object representing the CSIGN gate. @@ -369,7 +405,7 @@ def csign(*, dtype="csr"): return cz_gate(dtype=dtype) -def berkeley(*, dtype="dense"): +def berkeley(*, dtype=None): """ Quantum object representing the Berkeley gate. @@ -385,6 +421,7 @@ def berkeley(*, dtype="dense"): Quantum object representation of Berkeley gate """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense return Qobj( [ [np.cos(np.pi / 8), 0, 0, 1.0j * np.sin(np.pi / 8)], @@ -392,11 +429,12 @@ def berkeley(*, dtype="dense"): [0, 1.0j * np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], [1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], ], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=False, ).to(dtype) -def swapalpha(alpha, *, dtype="csr"): +def swapalpha(alpha, *, dtype=None): """ Quantum object representing the SWAPalpha gate. @@ -411,6 +449,7 @@ def swapalpha(alpha, *, dtype="csr"): swapalpha_gate : qobj Quantum object representation of SWAPalpha gate """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR phase = np.exp(1.0j * np.pi * alpha) return Qobj( [ @@ -419,11 +458,12 @@ def swapalpha(alpha, *, dtype="csr"): [0, 0.5 * (1 - phase), 0.5 * (1 + phase), 0], [0, 0, 0, 1], ], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=(phase.imag <= settings.core["atol"]), ).to(dtype) -def swap(*, dtype="csr"): +def swap(*, dtype=None): """Quantum object representing the SWAP gate. Parameters @@ -438,13 +478,15 @@ def swap(*, dtype="csr"): Quantum object representation of SWAP gate """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=True, ).to(dtype) -def iswap(*, dtype="csr"): +def iswap(*, dtype=None): """Quantum object representing the iSWAP gate. Parameters @@ -458,13 +500,15 @@ def iswap(*, dtype="csr"): iswap_gate : qobj Quantum object representation of iSWAP gate """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=False, ).to(dtype) -def sqrtswap(*, dtype="dense"): +def sqrtswap(*, dtype=None): """Quantum object representing the square root SWAP gate. Parameters @@ -479,6 +523,7 @@ def sqrtswap(*, dtype="dense"): Quantum object representation of square root SWAP gate """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( np.array( [ @@ -488,11 +533,12 @@ def sqrtswap(*, dtype="dense"): [0, 0, 0, 1], ] ), - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=False, ).to(dtype) -def sqrtiswap(*, dtype="dense"): +def sqrtiswap(*, dtype=None): """Quantum object representing the square root iSWAP gate. Parameters @@ -506,6 +552,7 @@ def sqrtiswap(*, dtype="dense"): sqrtiswap_gate : qobj Quantum object representation of square root iSWAP gate """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( np.array( [ @@ -515,11 +562,12 @@ def sqrtiswap(*, dtype="dense"): [0, 0, 0, 1], ] ), - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=False, ).to(dtype) -def molmer_sorensen(theta, *, dtype="dense"): +def molmer_sorensen(theta, *, dtype=None): """ Quantum object of a Mølmer–Sørensen gate. @@ -540,6 +588,7 @@ def molmer_sorensen(theta, *, dtype="dense"): molmer_sorensen_gate: :class:`.Qobj` Quantum object representation of the Mølmer–Sørensen gate. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [ [np.cos(theta / 2.0), 0, 0, -1.0j * np.sin(theta / 2.0)], @@ -547,7 +596,8 @@ def molmer_sorensen(theta, *, dtype="dense"): [0, -1.0j * np.sin(theta / 2.0), np.cos(theta / 2.0), 0], [-1.0j * np.sin(theta / 2.0), 0, 0, np.cos(theta / 2.0)], ], - dims=[[2, 2], [2, 2]], + dims=dims_2_qb, + isherm=(theta % (2 * np.pi) <= settings.core["atol"]), ).to(dtype) @@ -556,7 +606,10 @@ def molmer_sorensen(theta, *, dtype="dense"): # -def fredkin(*, dtype="csr"): +dims_3_qb = Dimensions([[2, 2, 2], [2, 2, 2]]) + + +def fredkin(*, dtype=None): """Quantum object representing the Fredkin gate. Parameters @@ -571,6 +624,7 @@ def fredkin(*, dtype="csr"): Quantum object representation of Fredkin gate. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [ [1, 0, 0, 0, 0, 0, 0, 0], @@ -582,11 +636,12 @@ def fredkin(*, dtype="csr"): [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], ], - dims=[[2, 2, 2], [2, 2, 2]], + dims=dims_3_qb, + isherm=True, ).to(dtype) -def toffoli(*, dtype="csr"): +def toffoli(*, dtype=None): """Quantum object representing the Toffoli gate. Parameters @@ -601,6 +656,7 @@ def toffoli(*, dtype="csr"): Quantum object representation of Toffoli gate. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [ [1, 0, 0, 0, 0, 0, 0, 0], @@ -612,7 +668,8 @@ def toffoli(*, dtype="csr"): [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 1, 0], ], - dims=[[2, 2, 2], [2, 2, 2]], + dims=dims_3_qb, + isherm=True, ).to(dtype) @@ -621,7 +678,7 @@ def toffoli(*, dtype="csr"): # -def globalphase(theta, N=1, *, dtype="csr"): +def globalphase(theta, N=1, *, dtype=None): """ Returns quantum object representing the global phase shift gate. @@ -640,6 +697,7 @@ def globalphase(theta, N=1, *, dtype="csr"): Quantum object representation of global phase shift gate. """ + dtype = dtype or settings.core["default_dtype"] or _data.CSR return qeye([2] * N, dtype=dtype) * np.exp(1.0j * theta) @@ -660,7 +718,7 @@ def _hamming_distance(x): return tot -def hadamard_transform(N=1, *, dtype="dense"): +def hadamard_transform(N=1, *, dtype=None): """Quantum object representing the N-qubit Hadamard gate. Parameters @@ -675,14 +733,12 @@ def hadamard_transform(N=1, *, dtype="dense"): Quantum object representation of the N-qubit Hadamard gate. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense data = 2 ** (-N / 2) * np.array( - [ - [(-1) ** _hamming_distance(i & j) for i in range(2**N)] - for j in range(2**N) - ] + [[(-1) ** _hamming_distance(i & j) for i in range(2**N)] for j in range(2**N)] ) - return Qobj(data, dims=[[2] * N, [2] * N]).to(dtype) + return Qobj(data, dims=[[2] * N, [2] * N], isherm=True).to(dtype) def _powers(op, N): @@ -698,7 +754,7 @@ def _powers(op, N): yield acc -def qubit_clifford_group(*, dtype="dense"): +def qubit_clifford_group(*, dtype=None): """ Generates the Clifford group on a single qubit, using the presentation of the group given by Ross and Selinger @@ -716,6 +772,7 @@ def qubit_clifford_group(*, dtype="dense"): Clifford operators, represented as Qobj instances. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense # The Ross-Selinger presentation of the single-qubit Clifford # group expresses each element in the form C_{ijk} = E^i X^j S^k @@ -738,10 +795,13 @@ def qubit_clifford_group(*, dtype="dense"): # product(...) yields the Cartesian product of its arguments. # Here, each element is a tuple (E**i, X**j, S**k) such that # partial(reduce, mul) acting on the tuple yields E**i * X**j * S**k. - return [ + gates = [ op.to(dtype) for op in map( partial(reduce, mul), product(_powers(E, 3), _powers(X, 2), _powers(S, 4)), ) ] + for gate in gates: + gate.isherm + return gates diff --git a/qutip/core/operators.py b/qutip/core/operators.py index c89cde6104..17ce618afe 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -12,14 +12,10 @@ 'tunneling', 'qft', 'qzero_like', 'qeye_like', 'swap', ] -import numbers - import numpy as np -import scipy.sparse - from . import data as _data from .qobj import Qobj -from .dimensions import flatten, Space +from .dimensions import Space from .. import settings @@ -64,8 +60,16 @@ def qdiags(diagonals, offsets=None, dims=None, shape=None, *, """ dtype = dtype or settings.core["default_dtype"] or _data.Dia offsets = [0] if offsets is None else offsets + if not isinstance(offsets, list): + offsets = [offsets] + if len(offsets) == 1 and offsets[0] != 0: + isherm = False + elif offsets == [0]: + isherm = np.all(np.imag(diagonals) <= settings.core["atol"]) + else: + isherm = None data = _data.diag[dtype](diagonals, offsets, shape) - return Qobj(data, dims=dims, copy=False) + return Qobj(data, dims=dims, copy=False, isherm=isherm) def jmat(j, which=None, *, dtype=None): @@ -305,7 +309,7 @@ def spin_J_set(j, *, dtype=None): _SIGMAZ = 2 * jmat(0.5, 'z') -def sigmap(): +def sigmap(*, dtype=None): """Creation operator for Pauli spins. Examples @@ -318,10 +322,11 @@ def sigmap(): [ 0. 0.]] """ - return _SIGMAP.copy() + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return _SIGMAP.to(dtype, True) -def sigmam(): +def sigmam(*, dtype=None): """Annihilation operator for Pauli spins. Examples @@ -334,10 +339,11 @@ def sigmam(): [ 1. 0.]] """ - return _SIGMAM.copy() + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return _SIGMAM.to(dtype, True) -def sigmax(): +def sigmax(*, dtype=None): """Pauli spin 1/2 sigma-x operator Examples @@ -350,10 +356,11 @@ def sigmax(): [ 1. 0.]] """ - return _SIGMAX.copy() + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return _SIGMAX.to(dtype, True) -def sigmay(): +def sigmay(*, dtype=None): """Pauli spin 1/2 sigma-y operator. Examples @@ -366,10 +373,11 @@ def sigmay(): [ 0.+1.j 0.+0.j]] """ - return _SIGMAY.copy() + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return _SIGMAY.to(dtype, True) -def sigmaz(): +def sigmaz(*, dtype=None): """Pauli spin 1/2 sigma-z operator. Examples @@ -382,7 +390,8 @@ def sigmaz(): [ 0. -1.]] """ - return _SIGMAZ.copy() + dtype = dtype or settings.core["default_dtype"] or _data.CSR + return _SIGMAZ.to(dtype, True) def destroy(N, offset=0, *, dtype=None): @@ -891,7 +900,9 @@ def squeeze(N, z, offset=0, *, dtype=None): """ asq = destroy(N, offset=offset, dtype=dtype) ** 2 op = 0.5*np.conj(z)*asq - 0.5*z*asq.dag() - return op.expm(dtype=dtype) + out = op.expm(dtype=dtype) + out.isherm = (N == 2) or (z == 0.) + return out def squeezing(a1, a2, z): @@ -961,7 +972,9 @@ def displace(N, alpha, offset=0, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense a = destroy(N, offset=offset) - return (alpha * a.dag() - np.conj(alpha) * a).expm(dtype=dtype) + out = (alpha * a.dag() - np.conj(alpha) * a).expm(dtype=dtype) + out.isherm = (alpha == 0.) + return out def commutator(A, B, kind="normal"): @@ -1049,7 +1062,7 @@ def phase(N, phi0=0, *, dtype=None): states = np.array([np.sqrt(kk) / np.sqrt(N) * np.exp(1j * n * kk) for kk in phim]) ops = np.sum([np.outer(st, st.conj()) for st in states], axis=0) - return Qobj(ops, dims=[[N], [N]], copy=False).to(dtype) + return Qobj(ops, isherm=True, dims=[[N], [N]], copy=False).to(dtype) def charge(Nmax, Nmin=None, frac=1, *, dtype=None): @@ -1147,7 +1160,7 @@ def qft(dimensions, *, dtype="dense"): arr = np.arange(N2) L, M = np.meshgrid(arr, arr) data = np.exp(phase * (L * M)) / np.sqrt(N2) - return Qobj(data, dims=[dimensions]*2).to(dtype) + return Qobj(data, isherm=False, dims=[dimensions]*2).to(dtype) def swap(N, M, *, dtype=None): @@ -1174,5 +1187,6 @@ def swap(N, M, *, dtype=None): cols = np.ravel(M * np.arange(N)[None, :] + np.arange(M)[:, None]) return Qobj( _data.CSR((data, cols, rows), (N * M, N * M)), - dims=[[M, N], [N, M]] + dims=[[M, N], [N, M]], + isherm=(N == M), ).to(dtype) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 4c498bc09a..c380e9d107 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -358,7 +358,7 @@ def data(self, data): f"{self._dims.shape} vs {data.shape}") self._data = data - def to(self, data_type): + def to(self, data_type, copy=False): """ Convert the underlying data store of this `Qobj` into a different storage representation. @@ -381,6 +381,9 @@ def to(self, data_type): The data-layer type that the data of this :class:`Qobj` should be converted to. + copy : Bool + Whether to return a copy if the data is not changed. + Returns ------- Qobj @@ -391,7 +394,9 @@ def to(self, data_type): converter = _data.to[data_type] except (KeyError, TypeError): raise ValueError("Unknown conversion type: " + str(data_type)) - if type(self._data) is data_type: + if type(self._data) is data_type and copy: + return self.copy() + elif type(self._data) is data_type: return self return Qobj(converter(self._data), dims=self._dims, diff --git a/qutip/tests/core/test_gates.py b/qutip/tests/core/test_gates.py index 00f47ffedd..a6af2cabb2 100644 --- a/qutip/tests/core/test_gates.py +++ b/qutip/tests/core/test_gates.py @@ -111,12 +111,18 @@ class TestCliffordGroup: Test a sufficient set of conditions to prove that we have a full Clifford group for a single qubit. """ - clifford = gates.qubit_clifford_group() + with qutip.CoreOptions(default_dtype="dia"): + clifford = gates.qubit_clifford_group() + pauli = [qutip.qeye(2), qutip.sigmax(), qutip.sigmay(), qutip.sigmaz()] def test_single_qubit_group_dimension_is_24(self): assert len(self.clifford) == 24 + def test_dtype(self): + for gate in self.clifford: + assert isinstance(gate.data, qutip.data.Dia) + def test_all_elements_different(self): clifford = [_remove_global_phase(gate) for gate in self.clifford] for i, gate in enumerate(clifford): @@ -124,7 +130,7 @@ def test_all_elements_different(self): # Big tolerance because we actually want to test the inverse. assert not np.allclose(gate.full(), other.full(), atol=1e-3) - @pytest.mark.parametrize("gate", gates.qubit_clifford_group()) + @pytest.mark.parametrize("gate", gates.qubit_clifford_group(dtype="dense")) def test_gate_normalises_pauli_group(self, gate): """ Test the fundamental definition of the Clifford group, i.e. that it @@ -133,6 +139,8 @@ def test_gate_normalises_pauli_group(self, gate): # Assert that each Clifford gate maps the set of Pauli gates back onto # itself (though not necessarily in order). This condition is no # stronger than simply considering each (gate, Pauli) pair separately. + assert gate._isherm == qutip.data.isherm(gate.data) + assert isinstance(gate.data, qutip.data.Dense) pauli_gates = [_remove_global_phase(x) for x in self.pauli] normalised = [_remove_global_phase(gate * pauli * gate.dag()) for pauli in self.pauli] @@ -142,3 +150,49 @@ def test_gate_normalises_pauli_group(self, gate): del pauli_gates[i] break assert len(pauli_gates) == 0 + + +@pytest.mark.parametrize("dtype", [qutip.data.Dense, qutip.data.CSR]) +@pytest.mark.parametrize(["gate_func", "args"], [ + pytest.param(gates.cnot, (), id="cnot"), + pytest.param(gates.cy_gate, (), id="cy_gate"), + pytest.param(gates.cz_gate, (), id="cz_gate"), + pytest.param(gates.cs_gate, (), id="cs_gate"), + pytest.param(gates.ct_gate, (), id="ct_gate"), + pytest.param(gates.s_gate, (), id="s_gate"), + pytest.param(gates.t_gate, (), id="t_gate"), + pytest.param(gates.cphase, (np.pi,), id="cphase"), + pytest.param(gates.csign, (), id="csign"), + pytest.param(gates.fredkin, (), id="fredkin"), + pytest.param(gates.toffoli, (), id="toffoli"), + pytest.param(gates.rx, (np.pi,), id="rx"), + pytest.param(gates.ry, (np.pi,), id="ry 1"), + pytest.param(gates.ry, (4 * np.pi,), id="ry 0"), + pytest.param(gates.rz, (1,), id="rz"), + pytest.param(gates.sqrtnot, (), id="sqrtnot"), + pytest.param(gates.snot, (), id="snot"), + pytest.param(gates.phasegate, (0,), id="phasegate 0"), + pytest.param(gates.phasegate, (1,), id="phasegate 1"), + pytest.param(gates.qrot, (0, 0), id="qrot id"), + pytest.param(gates.qrot, (2*np.pi, np.pi), id="qrot 0 pi"), + pytest.param(gates.qrot, (np.pi, 0), id="qrot pi 0"), + pytest.param(gates.qrot, (np.pi, np.pi), id="qrot pi pi"), + pytest.param(gates.berkeley, (), id="berkeley"), + pytest.param(gates.swapalpha, (0,), id="swapalpha 0"), + pytest.param(gates.swapalpha, (1,), id="swapalpha 1"), + pytest.param(gates.swap, (), id="swap"), + pytest.param(gates.iswap, (), id="iswap"), + pytest.param(gates.sqrtswap, (), id="sqrtswap"), + pytest.param(gates.sqrtiswap, (), id="sqrtiswap"), + pytest.param(gates.molmer_sorensen, (0,), id="molmer_sorensen 0"), + pytest.param(gates.molmer_sorensen, (np.pi,), id="molmer_sorensen pi"), + pytest.param(gates.hadamard_transform, (), id="hadamard_transform"), + ]) +def test_metadata(gate_func, args, dtype): + gate = gate_func(*args, dtype=dtype) + assert isinstance(gate.data, dtype) + assert gate._isherm == qutip.data.isherm(gate.data) + assert gate.isunitary + with qutip.CoreOptions(default_dtype=dtype): + gate = gate_func(*args) + assert isinstance(gate.data, dtype) From a50b70239b2ca026134ab6968d66b12e0a2ae630 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 9 Apr 2024 15:41:47 -0400 Subject: [PATCH 095/305] set isherm for all operators --- qutip/core/energy_restricted.py | 2 +- qutip/core/operators.py | 13 +++++++++-- qutip/tests/core/test_operators.py | 37 +++++++++++++++++++----------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/qutip/core/energy_restricted.py b/qutip/core/energy_restricted.py index f790b09140..14712949f1 100644 --- a/qutip/core/energy_restricted.py +++ b/qutip/core/energy_restricted.py @@ -247,7 +247,7 @@ def enr_destroy(dims, excitations, *, dtype=None): n2 = state2idx[state2] a_ops[idx][n2, n1] = np.sqrt(s) - return [Qobj(a, dims=enr_dims).to(dtype) for a in a_ops] + return [Qobj(a, dims=enr_dims, isherm=False).to(dtype) for a in a_ops] def enr_identity(dims, excitations, *, dtype=None): diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 17ce618afe..f7407e4aa1 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -624,7 +624,9 @@ def _f_op(n_sites, site, action, dtype=None): eye = identity(2, dtype=dtype) opers = [s_z] * site + [operator] + [eye] * (n_sites - site - 1) - return tensor(opers).to(dtype) + out = tensor(opers).to(dtype) + out.isherm = False + return out def qzero(dimensions, dims_right=None, *, dtype=None): @@ -1027,6 +1029,12 @@ def qutrit_ops(*, dtype=None): out[3] = one * two.dag() out[4] = two * three.dag() out[5] = three * one.dag() + out[0]._isherm = True + out[1]._isherm = True + out[2]._isherm = True + out[3]._isherm = False + out[4]._isherm = False + out[5]._isherm = False return out @@ -1132,7 +1140,7 @@ def tunneling(N, m=1, *, dtype=None): return T -def qft(dimensions, *, dtype="dense"): +def qft(dimensions, *, dtype=None): """ Quantum Fourier Transform operator. @@ -1153,6 +1161,7 @@ def qft(dimensions, *, dtype="dense"): Quantum Fourier transform operator. """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense dimensions = Space(dimensions) N2 = dimensions.size diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index 712194934f..97131dd790 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -246,11 +246,18 @@ def _id_func(val): return "" +def _check_meta(object, dtype): + if not isinstance(object, qutip.Qobj): + [_check_meta(qobj, dtype) for qobj in object] + return + assert isinstance(object.data, dtype) + assert object._isherm == qutip.data.isherm(object.data) + + # random object accept `str` and base.Data # Obtain all valid dtype from `to` dtype_names = list(qutip.data.to._str2type.keys()) + list(qutip.data.to.dtypes) -dtype_types = list(qutip.data.to._str2type.values()) + list(qutip.data.to.dtypes) -@pytest.mark.parametrize(['alias', 'dtype'], zip(dtype_names, dtype_types), +@pytest.mark.parametrize('alias', dtype_names, ids=[str(dtype) for dtype in dtype_names]) @pytest.mark.parametrize(['func', 'args'], [ (qutip.qdiags, ([0, 1, 2], 1)), @@ -258,7 +265,13 @@ def _id_func(val): (qutip.spin_Jx, (1,)), (qutip.spin_Jy, (1,)), (qutip.spin_Jz, (1,)), + (qutip.spin_Jm, (1,)), (qutip.spin_Jp, (1,)), + (qutip.sigmax, ()), + (qutip.sigmay, ()), + (qutip.sigmaz, ()), + (qutip.sigmap, ()), + (qutip.sigmam, ()), (qutip.destroy, (5,)), (qutip.create, (5,)), (qutip.fdestroy, (5, 0)), @@ -274,24 +287,19 @@ def _id_func(val): (qutip.phase, (5,)), (qutip.charge, (5,)), (qutip.tunneling, (5,)), + (qutip.qft, (5,)), + (qutip.swap, (3, 2)), (qutip.enr_destroy, ([3, 3, 3], 4)), (qutip.enr_identity, ([3, 3, 3], 4)), ], ids=_id_func) -def test_operator_type(func, args, alias, dtype): +def test_operator_type(func, args, alias): object = func(*args, dtype=alias) - if isinstance(object, qutip.Qobj): - assert isinstance(object.data, dtype) - else: - for obj in object: - assert isinstance(obj.data, dtype) + dtype = qutip.data.to.parse(alias) + _check_meta(object, dtype) with qutip.CoreOptions(default_dtype=alias): object = func(*args) - if isinstance(object, qutip.Qobj): - assert isinstance(object.data, dtype) - else: - for obj in object: - assert isinstance(obj.data, dtype) + _check_meta(object, dtype) @pytest.mark.parametrize('dims', [8, 15, [2] * 4]) @@ -331,6 +339,7 @@ def test_qeye_like(dims, superrep, dtype): expected.superrep = superrep assert new == expected assert new.dtype is qutip.data.to.parse(dtype) + assert new._isherm opevo = qutip.QobjEvo(op) new = qutip.qeye_like(op) @@ -360,6 +369,7 @@ def test_qzero_like(dims, superrep, dtype): expected.superrep = superrep assert new == expected assert new.dtype is qutip.data.to.parse(dtype) + assert new._isherm opevo = qutip.QobjEvo(op) new = qutip.qzero_like(op) @@ -387,6 +397,7 @@ def test_fcreate_fdestroy(n_sites): assert qutip.commutator(c_1, d_0, 'anti') == zero_tensor assert qutip.commutator(identity, c_0) == zero_tensor + @pytest.mark.parametrize(['func', 'args'], [ (qutip.qzero, (None,)), (qutip.fock, (None,)), From d5d271981bb02307272a404691293e3c98db82f8 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 9 Apr 2024 17:23:21 -0400 Subject: [PATCH 096/305] Add isunitary --- qutip/core/energy_restricted.py | 5 ++- qutip/core/gates.py | 31 ++++++++++++++--- qutip/core/operators.py | 53 +++++++++++++++++++----------- qutip/tests/core/test_gates.py | 2 +- qutip/tests/core/test_operators.py | 6 +++- 5 files changed, 70 insertions(+), 27 deletions(-) diff --git a/qutip/core/energy_restricted.py b/qutip/core/energy_restricted.py index 14712949f1..1a12f93b3e 100644 --- a/qutip/core/energy_restricted.py +++ b/qutip/core/energy_restricted.py @@ -247,7 +247,10 @@ def enr_destroy(dims, excitations, *, dtype=None): n2 = state2idx[state2] a_ops[idx][n2, n1] = np.sqrt(s) - return [Qobj(a, dims=enr_dims, isherm=False).to(dtype) for a in a_ops] + return [ + Qobj(a, dims=enr_dims, isunitary=False, isherm=False).to(dtype) + for a in a_ops + ] def enr_identity(dims, excitations, *, dtype=None): diff --git a/qutip/core/gates.py b/qutip/core/gates.py index e28e53173f..c8a0e0b290 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -61,6 +61,7 @@ def cy_gate(*, dtype=None): [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], dims=dims_2_qb, isherm=True, + isunitary=True, ).to(dtype) @@ -188,6 +189,7 @@ def rx(phi, *, dtype=None): [-1j * np.sin(phi / 2), np.cos(phi / 2)], ], isherm=(phi % (2 * np.pi) <= settings.core["atol"]), + isunitary=True, ).to(dtype) @@ -216,6 +218,7 @@ def ry(phi, *, dtype=None): [np.sin(phi / 2), np.cos(phi / 2)], ], isherm=(phi % (2 * np.pi) <= settings.core["atol"]), + isunitary=True, ).to(dtype) @@ -257,9 +260,11 @@ def sqrtnot(*, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense - return Qobj([[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]], isherm=False).to( - dtype - ) + return Qobj( + [[0.5 + 0.5j, 0.5 - 0.5j], [0.5 - 0.5j, 0.5 + 0.5j]], + isherm=False, + isunitary=True, + ).to(dtype) def snot(*, dtype=None): @@ -278,7 +283,11 @@ def snot(*, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - return Qobj([[1, 1], [1, -1]], isherm=True).to(dtype) / np.sqrt(2.0) + return Qobj( + [[np.sqrt(0.5), np.sqrt(0.5)], [np.sqrt(0.5), -np.sqrt(0.5)]], + isherm=True, + isunitary=True, + ).to(dtype) def phasegate(theta, *, dtype=None): @@ -330,6 +339,7 @@ def qrot(theta, phi, *, dtype=None): [-1j * np.exp(1j * phi) * np.sin(theta / 2), np.cos(theta / 2)], ], isherm=(theta % (2 * np.pi) <= settings.core["atol"]), + isunitary=True, ).to(dtype) @@ -383,6 +393,7 @@ def cnot(*, dtype=None): [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dims=dims_2_qb, isherm=True, + isunitary=True, ).to(dtype) @@ -431,6 +442,7 @@ def berkeley(*, dtype=None): ], dims=dims_2_qb, isherm=False, + isunitary=True, ).to(dtype) @@ -460,6 +472,7 @@ def swapalpha(alpha, *, dtype=None): ], dims=dims_2_qb, isherm=(phase.imag <= settings.core["atol"]), + isunitary=True, ).to(dtype) @@ -483,6 +496,7 @@ def swap(*, dtype=None): [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dims=dims_2_qb, isherm=True, + isunitary=True, ).to(dtype) @@ -505,6 +519,7 @@ def iswap(*, dtype=None): [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], dims=dims_2_qb, isherm=False, + isunitary=True, ).to(dtype) @@ -535,6 +550,7 @@ def sqrtswap(*, dtype=None): ), dims=dims_2_qb, isherm=False, + isunitary=True, ).to(dtype) @@ -564,6 +580,7 @@ def sqrtiswap(*, dtype=None): ), dims=dims_2_qb, isherm=False, + isunitary=True, ).to(dtype) @@ -598,6 +615,7 @@ def molmer_sorensen(theta, *, dtype=None): ], dims=dims_2_qb, isherm=(theta % (2 * np.pi) <= settings.core["atol"]), + isunitary=True, ).to(dtype) @@ -638,6 +656,7 @@ def fredkin(*, dtype=None): ], dims=dims_3_qb, isherm=True, + isunitary=True, ).to(dtype) @@ -670,6 +689,7 @@ def toffoli(*, dtype=None): ], dims=dims_3_qb, isherm=True, + isunitary=True, ).to(dtype) @@ -738,7 +758,7 @@ def hadamard_transform(N=1, *, dtype=None): [[(-1) ** _hamming_distance(i & j) for i in range(2**N)] for j in range(2**N)] ) - return Qobj(data, dims=[[2] * N, [2] * N], isherm=True).to(dtype) + return Qobj(data, dims=[[2] * N, [2] * N], isherm=True, isunitary=True).to(dtype) def _powers(op, N): @@ -804,4 +824,5 @@ def qubit_clifford_group(*, dtype=None): ] for gate in gates: gate.isherm + gate._isunitary = True return gates diff --git a/qutip/core/operators.py b/qutip/core/operators.py index f7407e4aa1..138868588c 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -64,12 +64,15 @@ def qdiags(diagonals, offsets=None, dims=None, shape=None, *, offsets = [offsets] if len(offsets) == 1 and offsets[0] != 0: isherm = False + isunitary = False elif offsets == [0]: isherm = np.all(np.imag(diagonals) <= settings.core["atol"]) + isunitary = np.all(np.abs(diagonals) - 1 <= settings.core["atol"]) else: isherm = None + isunitary = None data = _data.diag[dtype](diagonals, offsets, shape) - return Qobj(data, dims=dims, copy=False, isherm=isherm) + return Qobj(data, dims=dims, copy=False, isherm=isherm, isunitary=isunitary) def jmat(j, which=None, *, dtype=None): @@ -136,8 +139,8 @@ def jmat(j, which=None, *, dtype=None): isherm=False, isunitary=False, copy=False) if which == 'x': A = _jplus(j, dtype=dtype) - return Qobj(_data.add(A, A.adjoint()), dims=dims, - isherm=True, isunitary=False, copy=False) * 0.5 + return Qobj(_data.add(A, A.adjoint()) * 0.5, dims=dims, + isherm=True, isunitary=False, copy=False) if which == 'y': A = _data.mul(_jplus(j, dtype=dtype), -0.5j) return Qobj(_data.add(A, A.adjoint()), dims=dims, @@ -305,8 +308,11 @@ def spin_J_set(j, *, dtype=None): _SIGMAP = jmat(0.5, '+') _SIGMAM = jmat(0.5, '-') _SIGMAX = 2 * jmat(0.5, 'x') +_SIGMAX._isunitary = True _SIGMAY = 2 * jmat(0.5, 'y') +_SIGMAY._isunitary = True _SIGMAZ = 2 * jmat(0.5, 'z') +_SIGMAZ._isunitary = True def sigmap(*, dtype=None): @@ -626,6 +632,7 @@ def _f_op(n_sites, site, action, dtype=None): opers = [s_z] * site + [operator] + [eye] * (n_sites - site - 1) out = tensor(opers).to(dtype) out.isherm = False + out._isunitary = False return out @@ -794,6 +801,7 @@ def position(N, offset=0, *, dtype=None): a = destroy(N, offset=offset, dtype=dtype) position = np.sqrt(0.5) * (a + a.dag()) position.isherm = True + position._isunitary = False return position.to(dtype) @@ -823,6 +831,7 @@ def momentum(N, offset=0, *, dtype=None): a = destroy(N, offset=offset, dtype=dtype) momentum = -1j * np.sqrt(0.5) * (a - a.dag()) momentum.isherm = True + momentum._isunitary = False return momentum.to(dtype) @@ -904,6 +913,7 @@ def squeeze(N, z, offset=0, *, dtype=None): op = 0.5*np.conj(z)*asq - 0.5*z*asq.dag() out = op.expm(dtype=dtype) out.isherm = (N == 2) or (z == 0.) + out._isunitary = True return out @@ -976,6 +986,7 @@ def displace(N, alpha, offset=0, *, dtype=None): a = destroy(N, offset=offset) out = (alpha * a.dag() - np.conj(alpha) * a).expm(dtype=dtype) out.isherm = (alpha == 0.) + out._isunitary = True return out @@ -1022,19 +1033,14 @@ def qutrit_ops(*, dtype=None): dtype = dtype or settings.core["default_dtype"] or _data.CSR out = np.empty((6,), dtype=object) - one, two, three = qutrit_basis(dtype=dtype) - out[0] = one * one.dag() - out[1] = two * two.dag() - out[2] = three * three.dag() - out[3] = one * two.dag() - out[4] = two * three.dag() - out[5] = three * one.dag() - out[0]._isherm = True - out[1]._isherm = True - out[2]._isherm = True - out[3]._isherm = False - out[4]._isherm = False - out[5]._isherm = False + basis = qutrit_basis(dtype=dtype) + for i in range(3): + out[i] = basis[i] @ basis[i].dag() + out[i].isherm = True + out[i]._isunitary = False + out[i+3] = basis[i] @ basis[(i+1)%3].dag() + out[i+3].isherm = False + out[i+3]._isunitary = False return out @@ -1070,7 +1076,13 @@ def phase(N, phi0=0, *, dtype=None): states = np.array([np.sqrt(kk) / np.sqrt(N) * np.exp(1j * n * kk) for kk in phim]) ops = np.sum([np.outer(st, st.conj()) for st in states], axis=0) - return Qobj(ops, isherm=True, dims=[[N], [N]], copy=False).to(dtype) + return Qobj( + ops, + isherm=True, + isunitary=False, + dims=[[N], [N]], + copy=False + ).to(dtype) def charge(Nmax, Nmin=None, frac=1, *, dtype=None): @@ -1108,7 +1120,7 @@ def charge(Nmax, Nmin=None, frac=1, *, dtype=None): Nmin = -Nmax diag = frac * np.arange(Nmin, Nmax+1, dtype=float) out = qdiags(diag, 0, dtype=dtype) - out.isherm = True + out._isunitary = (len(diag) <= 2) and np.all(np.abs(diag) == 1.) return out @@ -1137,6 +1149,7 @@ def tunneling(N, m=1, *, dtype=None): diags = [np.ones(N-m, dtype=int), np.ones(N-m, dtype=int)] T = qdiags(diags, [m, -m], dtype=dtype) T.isherm = True + T._isunitary = (m * 2 == N) return T @@ -1163,13 +1176,14 @@ def qft(dimensions, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense dimensions = Space(dimensions) + dims = [dimensions]*2 N2 = dimensions.size phase = 2.0j * np.pi / N2 arr = np.arange(N2) L, M = np.meshgrid(arr, arr) data = np.exp(phase * (L * M)) / np.sqrt(N2) - return Qobj(data, isherm=False, dims=[dimensions]*2).to(dtype) + return Qobj(data, isherm=False, isunitary=True, dims=dims).to(dtype) def swap(N, M, *, dtype=None): @@ -1198,4 +1212,5 @@ def swap(N, M, *, dtype=None): _data.CSR((data, cols, rows), (N * M, N * M)), dims=[[M, N], [N, M]], isherm=(N == M), + isunitary=True, ).to(dtype) diff --git a/qutip/tests/core/test_gates.py b/qutip/tests/core/test_gates.py index a6af2cabb2..8686daf0d9 100644 --- a/qutip/tests/core/test_gates.py +++ b/qutip/tests/core/test_gates.py @@ -192,7 +192,7 @@ def test_metadata(gate_func, args, dtype): gate = gate_func(*args, dtype=dtype) assert isinstance(gate.data, dtype) assert gate._isherm == qutip.data.isherm(gate.data) - assert gate.isunitary + assert gate._isunitary == gate._calculate_isunitary() with qutip.CoreOptions(default_dtype=dtype): gate = gate_func(*args) assert isinstance(gate.data, dtype) diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index 97131dd790..f200454c0a 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -252,11 +252,12 @@ def _check_meta(object, dtype): return assert isinstance(object.data, dtype) assert object._isherm == qutip.data.isherm(object.data) + assert object._isunitary == object._calculate_isunitary() # random object accept `str` and base.Data # Obtain all valid dtype from `to` -dtype_names = list(qutip.data.to._str2type.keys()) + list(qutip.data.to.dtypes) +dtype_names = ["dense", "csr"] + list(qutip.data.to.dtypes) @pytest.mark.parametrize('alias', dtype_names, ids=[str(dtype) for dtype in dtype_names]) @pytest.mark.parametrize(['func', 'args'], [ @@ -286,8 +287,11 @@ def _check_meta(object, dtype): (qutip.qutrit_ops, ()), (qutip.phase, (5,)), (qutip.charge, (5,)), + (qutip.charge, (0.5, -0.5, 2.)), (qutip.tunneling, (5,)), + (qutip.tunneling, (4, 2)), (qutip.qft, (5,)), + (qutip.swap, (2, 2)), (qutip.swap, (3, 2)), (qutip.enr_destroy, ([3, 3, 3], 4)), (qutip.enr_identity, ([3, 3, 3], 4)), From 06b211d3c8c6a979380f9fb821e9d90da9a74d89 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 9 Apr 2024 17:53:07 -0400 Subject: [PATCH 097/305] Add towncrier --- doc/changes/2388.misc | 1 + qutip/tests/core/test_operators.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 doc/changes/2388.misc diff --git a/doc/changes/2388.misc b/doc/changes/2388.misc new file mode 100644 index 0000000000..f976819369 --- /dev/null +++ b/doc/changes/2388.misc @@ -0,0 +1 @@ +Better metadata management in operators creation functions \ No newline at end of file diff --git a/qutip/tests/core/test_operators.py b/qutip/tests/core/test_operators.py index f200454c0a..477d7da8a4 100644 --- a/qutip/tests/core/test_operators.py +++ b/qutip/tests/core/test_operators.py @@ -256,7 +256,6 @@ def _check_meta(object, dtype): # random object accept `str` and base.Data -# Obtain all valid dtype from `to` dtype_names = ["dense", "csr"] + list(qutip.data.to.dtypes) @pytest.mark.parametrize('alias', dtype_names, ids=[str(dtype) for dtype in dtype_names]) From 18bbc8201491b2a6d487bfe94b0058d9c94b32bd Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 9 Apr 2024 20:38:24 -0400 Subject: [PATCH 098/305] Fix from comment --- qutip/core/gates.py | 45 +++++++++++++++++----------------- qutip/core/operators.py | 30 +++++++++++++++++++++++ qutip/core/qobj.py | 33 +++++++++++-------------- qutip/entropy.py | 1 + qutip/tests/core/test_gates.py | 9 ++++--- qutip/tests/test_entropy.py | 2 ++ 6 files changed, 75 insertions(+), 45 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index c8a0e0b290..12441b4168 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -42,6 +42,10 @@ ] +_DIMS_2_QB = Dimensions([[2, 2], [2, 2]]) +_DIMS_3_QB = Dimensions([[2, 2, 2], [2, 2, 2]]) + + def cy_gate(*, dtype=None): """Controlled Y gate. @@ -59,7 +63,7 @@ def cy_gate(*, dtype=None): dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]], - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=True, isunitary=True, ).to(dtype) @@ -80,7 +84,7 @@ def cz_gate(*, dtype=None): Quantum object for operator describing the rotation. """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - return qdiags([1, 1, 1, -1], dims=dims_2_qb, dtype=dtype) + return qdiags([1, 1, 1, -1], dims=_DIMS_2_QB, dtype=dtype) def s_gate(*, dtype=None): @@ -119,7 +123,7 @@ def cs_gate(*, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - return qdiags([1, 1, 1, 1j], dims=dims_2_qb, dtype=dtype) + return qdiags([1, 1, 1, 1j], dims=_DIMS_2_QB, dtype=dtype) def t_gate(*, dtype=None): @@ -159,7 +163,7 @@ def ct_gate(*, dtype=None): dtype = dtype or settings.core["default_dtype"] or _data.CSR return qdiags( [1, 1, 1, np.exp(1j * np.pi / 4)], - dims=dims_2_qb, + dims=_DIMS_2_QB, dtype=dtype, ) @@ -348,9 +352,6 @@ def qrot(theta, phi, *, dtype=None): # -dims_2_qb = Dimensions([[2, 2], [2, 2]]) - - def cphase(theta, *, dtype=None): """ Returns quantum object representing the controlled phase shift gate. @@ -369,7 +370,7 @@ def cphase(theta, *, dtype=None): Quantum object representation of controlled phase gate. """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - return qdiags([1, 1, 1, np.exp(1.0j * theta)], dims=dims_2_qb, dtype=dtype) + return qdiags([1, 1, 1, np.exp(1.0j * theta)], dims=_DIMS_2_QB, dtype=dtype) def cnot(*, dtype=None): @@ -391,7 +392,7 @@ def cnot(*, dtype=None): dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=True, isunitary=True, ).to(dtype) @@ -440,7 +441,7 @@ def berkeley(*, dtype=None): [0, 1.0j * np.sin(3 * np.pi / 8), np.cos(3 * np.pi / 8), 0], [1.0j * np.sin(np.pi / 8), 0, 0, np.cos(np.pi / 8)], ], - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=False, isunitary=True, ).to(dtype) @@ -452,6 +453,9 @@ def swapalpha(alpha, *, dtype=None): Parameters ---------- + alpha : float + Angle of the SWAPalpha gate. + dtype : str or type, [keyword only] [optional] Storage representation. Any data-layer known to `qutip.data.to` is accepted. @@ -470,8 +474,8 @@ def swapalpha(alpha, *, dtype=None): [0, 0.5 * (1 - phase), 0.5 * (1 + phase), 0], [0, 0, 0, 1], ], - dims=dims_2_qb, - isherm=(phase.imag <= settings.core["atol"]), + dims=_DIMS_2_QB, + isherm=(np.abs(phase.imag) <= settings.core["atol"]), isunitary=True, ).to(dtype) @@ -494,7 +498,7 @@ def swap(*, dtype=None): dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=True, isunitary=True, ).to(dtype) @@ -517,7 +521,7 @@ def iswap(*, dtype=None): dtype = dtype or settings.core["default_dtype"] or _data.CSR return Qobj( [[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=False, isunitary=True, ).to(dtype) @@ -548,7 +552,7 @@ def sqrtswap(*, dtype=None): [0, 0, 0, 1], ] ), - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=False, isunitary=True, ).to(dtype) @@ -578,7 +582,7 @@ def sqrtiswap(*, dtype=None): [0, 0, 0, 1], ] ), - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=False, isunitary=True, ).to(dtype) @@ -613,7 +617,7 @@ def molmer_sorensen(theta, *, dtype=None): [0, -1.0j * np.sin(theta / 2.0), np.cos(theta / 2.0), 0], [-1.0j * np.sin(theta / 2.0), 0, 0, np.cos(theta / 2.0)], ], - dims=dims_2_qb, + dims=_DIMS_2_QB, isherm=(theta % (2 * np.pi) <= settings.core["atol"]), isunitary=True, ).to(dtype) @@ -624,9 +628,6 @@ def molmer_sorensen(theta, *, dtype=None): # -dims_3_qb = Dimensions([[2, 2, 2], [2, 2, 2]]) - - def fredkin(*, dtype=None): """Quantum object representing the Fredkin gate. @@ -654,7 +655,7 @@ def fredkin(*, dtype=None): [0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], ], - dims=dims_3_qb, + dims=_DIMS_3_QB, isherm=True, isunitary=True, ).to(dtype) @@ -687,7 +688,7 @@ def toffoli(*, dtype=None): [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 1, 0], ], - dims=dims_3_qb, + dims=_DIMS_3_QB, isherm=True, isunitary=True, ).to(dtype) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 138868588c..b27b3c7ae8 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -318,6 +318,12 @@ def spin_J_set(j, *, dtype=None): def sigmap(*, dtype=None): """Creation operator for Pauli spins. + Parameters + ---------- + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Examples -------- >>> sigmap() # doctest: +SKIP @@ -335,6 +341,12 @@ def sigmap(*, dtype=None): def sigmam(*, dtype=None): """Annihilation operator for Pauli spins. + Parameters + ---------- + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Examples -------- >>> sigmam() # doctest: +SKIP @@ -352,6 +364,12 @@ def sigmam(*, dtype=None): def sigmax(*, dtype=None): """Pauli spin 1/2 sigma-x operator + Parameters + ---------- + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Examples -------- >>> sigmax() # doctest: +SKIP @@ -369,6 +387,12 @@ def sigmax(*, dtype=None): def sigmay(*, dtype=None): """Pauli spin 1/2 sigma-y operator. + Parameters + ---------- + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Examples -------- >>> sigmay() # doctest: +SKIP @@ -386,6 +410,12 @@ def sigmay(*, dtype=None): def sigmaz(*, dtype=None): """Pauli spin 1/2 sigma-z operator. + Parameters + ---------- + dtype : type or str, optional + Storage representation. Any data-layer known to ``qutip.data.to`` is + accepted. + Examples -------- >>> sigmaz() # doctest: +SKIP diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index c380e9d107..d6c0284930 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -371,38 +371,33 @@ def to(self, data_type, copy=False): algorithms and operations may be faster or more accurate when using a more appropriate data store. - If the data store is already in the format requested, the function - returns `self`. Otherwise, it returns a copy of itself with the data - store in the new type. - Parameters ---------- - data_type : type - The data-layer type that the data of this :class:`Qobj` should be - converted to. + data_type : type, str + The data-layer type or it's string alias that the data of this + :class:`Qobj` should be converted to. copy : Bool - Whether to return a copy if the data is not changed. + If the data store is already in the format requested, whether the + function should return returns `self` or a copy. Returns ------- Qobj - A new :class:`Qobj` if a type conversion took place with the data - stored in the requested format, or `self` if not. + A :class:`Qobj` with the data stored in the requested format. """ - try: - converter = _data.to[data_type] - except (KeyError, TypeError): - raise ValueError("Unknown conversion type: " + str(data_type)) + data_type = _data.to.parse(data_type) if type(self._data) is data_type and copy: return self.copy() elif type(self._data) is data_type: return self - return Qobj(converter(self._data), - dims=self._dims, - isherm=self._isherm, - isunitary=self._isunitary, - copy=False) + return Qobj( + _data.to(data_type, self._data), + dims=self._dims, + isherm=self._isherm, + isunitary=self._isunitary, + copy=False + ) @_require_equal_type def __add__(self, other): diff --git a/qutip/entropy.py b/qutip/entropy.py index 6f3f5cc7a6..a2cde5d33b 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -372,5 +372,6 @@ def entangling_power(U): a = tensor(U, U).dag() * swap13 * tensor(U, U) * swap13 Uswap = swap() * U b = tensor(Uswap, Uswap).dag() * swap13 * tensor(Uswap, Uswap) * swap13 + print(a.tr(), b.tr()) return 5.0/9 - 1.0/36 * (a.tr() + b.tr()).real diff --git a/qutip/tests/core/test_gates.py b/qutip/tests/core/test_gates.py index 8686daf0d9..27f01cdf9d 100644 --- a/qutip/tests/core/test_gates.py +++ b/qutip/tests/core/test_gates.py @@ -152,7 +152,7 @@ def test_gate_normalises_pauli_group(self, gate): assert len(pauli_gates) == 0 -@pytest.mark.parametrize("dtype", [qutip.data.Dense, qutip.data.CSR]) +@pytest.mark.parametrize("alias", [qutip.data.Dense, "CSR"]) @pytest.mark.parametrize(["gate_func", "args"], [ pytest.param(gates.cnot, (), id="cnot"), pytest.param(gates.cy_gate, (), id="cy_gate"), @@ -188,11 +188,12 @@ def test_gate_normalises_pauli_group(self, gate): pytest.param(gates.molmer_sorensen, (np.pi,), id="molmer_sorensen pi"), pytest.param(gates.hadamard_transform, (), id="hadamard_transform"), ]) -def test_metadata(gate_func, args, dtype): - gate = gate_func(*args, dtype=dtype) +def test_metadata(gate_func, args, alias): + gate = gate_func(*args, dtype=alias) + dtype = qutip.data.to.parse(alias) assert isinstance(gate.data, dtype) assert gate._isherm == qutip.data.isherm(gate.data) assert gate._isunitary == gate._calculate_isunitary() - with qutip.CoreOptions(default_dtype=dtype): + with qutip.CoreOptions(default_dtype=alias): gate = gate_func(*args) assert isinstance(gate.data, dtype) diff --git a/qutip/tests/test_entropy.py b/qutip/tests/test_entropy.py index ecf465c9bf..b0c4967f4e 100644 --- a/qutip/tests/test_entropy.py +++ b/qutip/tests/test_entropy.py @@ -209,4 +209,6 @@ def test_triangle_inequality_4_qubits(self): np.sin(np.pi*_alpha)**2 / 6, id="SWAP(alpha)"), ]) def test_entangling_power(gate, expected): + print("_alpha", _alpha) + print("gate", gate) assert abs(qutip.entangling_power(gate) - expected) < 1e-12 From 7edf0832536bf251ed83f5fa4c2493c60ff57f73 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 10 Apr 2024 08:46:58 -0400 Subject: [PATCH 099/305] Fix line width --- qutip/core/gates.py | 13 ++++++++++--- qutip/core/operators.py | 5 ++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index 12441b4168..9fc1bb9c71 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -370,7 +370,9 @@ def cphase(theta, *, dtype=None): Quantum object representation of controlled phase gate. """ dtype = dtype or settings.core["default_dtype"] or _data.CSR - return qdiags([1, 1, 1, np.exp(1.0j * theta)], dims=_DIMS_2_QB, dtype=dtype) + return qdiags( + [1, 1, 1, np.exp(1.0j * theta)], dims=_DIMS_2_QB, dtype=dtype + ) def cnot(*, dtype=None): @@ -756,10 +758,15 @@ def hadamard_transform(N=1, *, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense data = 2 ** (-N / 2) * np.array( - [[(-1) ** _hamming_distance(i & j) for i in range(2**N)] for j in range(2**N)] + [ + [(-1) ** _hamming_distance(i & j) for i in range(2**N)] + for j in range(2**N) + ] ) - return Qobj(data, dims=[[2] * N, [2] * N], isherm=True, isunitary=True).to(dtype) + return Qobj(data, dims=[[2] * N, [2] * N], isherm=True, isunitary=True).to( + dtype + ) def _powers(op, N): diff --git a/qutip/core/operators.py b/qutip/core/operators.py index b27b3c7ae8..94e2e2bcb2 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -72,7 +72,10 @@ def qdiags(diagonals, offsets=None, dims=None, shape=None, *, isherm = None isunitary = None data = _data.diag[dtype](diagonals, offsets, shape) - return Qobj(data, dims=dims, copy=False, isherm=isherm, isunitary=isunitary) + return Qobj( + data, copy=False, + dims=dims, isherm=isherm, isunitary=isunitary + ) def jmat(j, which=None, *, dtype=None): From 7ee2f71f20fdfb607b29e21321bba13ac1adf24d Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Thu, 11 Apr 2024 11:06:49 +0530 Subject: [PATCH 100/305] update the repo --- qutip/entropy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutip/entropy.py b/qutip/entropy.py index 26f3ae89ef..7a5d8a7dc0 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -143,6 +143,7 @@ def negativity(rho, subsys, method='tracenorm', logarithmic=False): else: raise ValueError("Unknown method %s" % method) +# Return the negativity value (or its logarithm if specified) if logarithmic: return log2(2 * N + 1) else: From f6f539cd443a0c3afa5b148092542ece70672eac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Apr 2024 02:17:07 +0000 Subject: [PATCH 101/305] Bump idna from 3.4 to 3.7 in /doc Bumps [idna](https://github.com/kjd/idna) from 3.4 to 3.7. - [Release notes](https://github.com/kjd/idna/releases) - [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.rst) - [Commits](https://github.com/kjd/idna/compare/v3.4...v3.7) --- updated-dependencies: - dependency-name: idna dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 62f907c792..ac2761b160 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -7,7 +7,7 @@ cycler==0.10.0 Cython==0.29.33 decorator==5.1.1 docutils==0.18.1 -idna==3.4 +idna==3.7 imagesize==1.4.1 ipython==8.11.0 jedi==0.18.2 From 2326f0222539f4e38e2231fe3413c64950a494d9 Mon Sep 17 00:00:00 2001 From: Neill Lambert Date: Fri, 12 Apr 2024 17:23:17 +0900 Subject: [PATCH 102/305] fix two bugs in floquet steadystate Two small bugs appeared in floquet steadystate in v5, this is a quick patch. --- qutip/solver/steadystate.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py index c484d782ae..9f600fc39e 100644 --- a/qutip/solver/steadystate.py +++ b/qutip/solver/steadystate.py @@ -373,18 +373,21 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, Notes ----- See: Sze Meng Tan, - https://copilot.caltech.edu/documents/16743/qousersguide.pdf, - Section (10.16) + https://painterlab.caltech.edu/wp-content/uploads/2019/06/qe_quantum_optics_toolbox.pdf, + Section (16) """ L_0 = liouvillian(H_0, c_ops) - L_m = L_p = 0.5 * liouvillian(Op_t) + L_m = 0.5 * liouvillian(Op_t) + L_p = 0.5 * liouvillian(Op_t) # L_p and L_m correspond to the positive and negative # frequency terms respectively. # They are independent in the model, so we keep both names. Id = qeye_like(L_0) - S = T = qzero_like(L_0) + S = qzero_like(L_0) + T = qzero_like(L_0) + if isinstance(H_0.data, _data.CSR) and not sparse: L_0 = L_0.to("Dense") @@ -395,7 +398,7 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, for n_i in np.arange(n_it, 0, -1): L = L_0 - 1j * n_i * w_d * Id + L_m @ S S.data = - _data.solve(L.data, L_p.data, solver, kwargs) - L = L_0 - 1j * n_i * w_d * Id + L_p @ T + L = L_0 + 1j * n_i * w_d * Id + L_p @ T T.data = - _data.solve(L.data, L_m.data, solver, kwargs) M_subs = L_0 + L_m @ S + L_p @ T From a4393eb53f4859e2905619316df6f57f08d1383e Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Sat, 13 Apr 2024 01:43:01 +0530 Subject: [PATCH 103/305] Solve failed test case --- doc/changes/2371.bugfix | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/changes/2371.bugfix diff --git a/doc/changes/2371.bugfix b/doc/changes/2371.bugfix new file mode 100644 index 0000000000..a40bea3550 --- /dev/null +++ b/doc/changes/2371.bugfix @@ -0,0 +1,4 @@ +Add your info here +The import statement was added to import the partial_transpose function directly from the qutip module. This was done to fix the TypeError: 'module' object is not callable error. + +Added a condition that check if the input rho is a ket (a type of quantum state) and it also ensures that the negativity function can handle both kets and density operators as input. \ No newline at end of file From 3ecdc1027fc2eef17c02533435f2207c07538260 Mon Sep 17 00:00:00 2001 From: vikas-chaudhary-2802 Date: Sat, 13 Apr 2024 15:52:06 +0530 Subject: [PATCH 104/305] remove line --- doc/changes/2371.bugfix | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/changes/2371.bugfix b/doc/changes/2371.bugfix index a40bea3550..85875e8b6a 100644 --- a/doc/changes/2371.bugfix +++ b/doc/changes/2371.bugfix @@ -1,4 +1,3 @@ -Add your info here The import statement was added to import the partial_transpose function directly from the qutip module. This was done to fix the TypeError: 'module' object is not callable error. Added a condition that check if the input rho is a ket (a type of quantum state) and it also ensures that the negativity function can handle both kets and density operators as input. \ No newline at end of file From d601702e51ac4c7c2cef8737423041d7a49dd5d1 Mon Sep 17 00:00:00 2001 From: Neill Lambert Date: Sat, 13 Apr 2024 21:07:35 +0900 Subject: [PATCH 105/305] adjusted floquet steasdystate test, fixed some pep 8 issues, added towncrier entry --- doc/changes/2393.bugfix | 1 + qutip/solver/steadystate.py | 8 +++---- qutip/tests/solver/test_steadystate.py | 31 +++++++++++++------------- 3 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 doc/changes/2393.bugfix diff --git a/doc/changes/2393.bugfix b/doc/changes/2393.bugfix new file mode 100644 index 0000000000..201651b614 --- /dev/null +++ b/doc/changes/2393.bugfix @@ -0,0 +1 @@ +Fix two bugs in steadystate floquet solver, and adjust tests to be sensitive to this issue. \ No newline at end of file diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py index 9f600fc39e..7f1424f7f6 100644 --- a/qutip/solver/steadystate.py +++ b/qutip/solver/steadystate.py @@ -356,9 +356,10 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, - "mkl_spsolve" sparse solver by mkl. - Extensions to qutip, such as qutip-tensorflow, may provide their own solvers. - When ``H_0`` and ``c_ops`` use these data backends, see their documentation - for the names and details of additional solvers they may provide. + Extensions to qutip, such as qutip-tensorflow, may provide their own + solvers. When ``H_0`` and ``c_ops`` use these data backends, see their + documentation for the names and details of additional solvers they may + provide. **kwargs: Extra options to pass to the linear system solver. See the @@ -388,7 +389,6 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False, S = qzero_like(L_0) T = qzero_like(L_0) - if isinstance(H_0.data, _data.CSR) and not sparse: L_0 = L_0.to("Dense") L_m = L_m.to("Dense") diff --git a/qutip/tests/solver/test_steadystate.py b/qutip/tests/solver/test_steadystate.py index 161e904e95..c5225b5c76 100644 --- a/qutip/tests/solver/test_steadystate.py +++ b/qutip/tests/solver/test_steadystate.py @@ -194,39 +194,40 @@ def test_steadystate_floquet(sparse): Test the steadystate solution for a periodically driven system. """ - N_c = 20 - - a = qutip.destroy(N_c) - a_d = a.dag() - X_c = a + a_d + sz = qutip.sigmaz() + sx = qutip.sigmax() w_c = 1 - - A_l = 0.001 + A_l = 0.5 w_l = w_c gam = 0.01 - H = w_c * a_d * a + H = w_c * sz - H_t = [H, [X_c, lambda t, args: args["A_l"] * np.cos(args["w_l"] * t)]] + H_t = [H, [sx, lambda t, args: args["A_l"] * np.cos(args["w_l"] * t)]] - psi0 = qutip.fock(N_c, 0) + psi0 = qutip.basis(2, 0) args = {"A_l": A_l, "w_l": w_l} c_ops = [] - c_ops.append(np.sqrt(gam) * a) + c_ops.append(np.sqrt(gam) * qutip.destroy(2).dag()) t_l = np.linspace(0, 20 / gam, 2000) expect_me = qutip.mesolve(H_t, psi0, t_l, - c_ops, [a_d * a], args=args).expect[0] + c_ops, [sz], args=args).expect[0] rho_ss = qutip.steadystate_floquet(H, c_ops, - A_l * X_c, w_l, n_it=3, sparse=sparse) - expect_ss = qutip.expect(a_d * a, rho_ss) + A_l * sx, w_l, n_it=3, sparse=sparse) + expect_ss = qutip.expect(sz, rho_ss) + + dt = (20 / gam) / len(t_l) + one_period = int(1/(w_l/(2*np.pi)) / dt) + + average_ex = sum(expect_me[-one_period:]) / float(one_period) - np.testing.assert_allclose(expect_me[-20:], expect_ss, atol=1e-3) + np.testing.assert_allclose(average_ex, expect_ss, atol=1e-2) assert rho_ss.tr() == pytest.approx(1, abs=1e-15) From 427e47bef4ec3b4cbaa7fd3d007365fa3122bd29 Mon Sep 17 00:00:00 2001 From: Neill Lambert Date: Mon, 15 Apr 2024 09:00:51 +0900 Subject: [PATCH 106/305] improve test Make test drive resonant and add more data points to allow for better comparison to mesolve --- qutip/tests/solver/test_steadystate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qutip/tests/solver/test_steadystate.py b/qutip/tests/solver/test_steadystate.py index c5225b5c76..df87f4f440 100644 --- a/qutip/tests/solver/test_steadystate.py +++ b/qutip/tests/solver/test_steadystate.py @@ -202,7 +202,7 @@ def test_steadystate_floquet(sparse): w_l = w_c gam = 0.01 - H = w_c * sz + H = 0.5 * w_c * sz H_t = [H, [sx, lambda t, args: args["A_l"] * np.cos(args["w_l"] * t)]] @@ -212,8 +212,8 @@ def test_steadystate_floquet(sparse): c_ops = [] c_ops.append(np.sqrt(gam) * qutip.destroy(2).dag()) - - t_l = np.linspace(0, 20 / gam, 2000) + T_max = 20 * 2 * np.pi / gam + t_l = np.linspace(0, T_max, 20000) expect_me = qutip.mesolve(H_t, psi0, t_l, c_ops, [sz], args=args).expect[0] @@ -222,7 +222,7 @@ def test_steadystate_floquet(sparse): A_l * sx, w_l, n_it=3, sparse=sparse) expect_ss = qutip.expect(sz, rho_ss) - dt = (20 / gam) / len(t_l) + dt = T_max / len(t_l) one_period = int(1/(w_l/(2*np.pi)) / dt) average_ex = sum(expect_me[-one_period:]) / float(one_period) From 9208fead8b1b5aa20959b73120664132fa1b6100 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 18 Apr 2024 13:52:46 +0900 Subject: [PATCH 107/305] Fixed target tolerance calculation --- qutip/solver/result.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 74c47734c0..923b377c3c 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -665,6 +665,11 @@ def _target_tolerance_end(self): Return the approximate number of trajectories needed to have this error within the tolerance fot all e_ops and times. """ + if self.num_trajectories >= self._target_ntraj: + # First make sure that "ntraj" setting is always respected + self.stats["end_condition"] = "ntraj reached" + return 0 + if self._num_rel_trajectories <= 1: return np.inf avg, avg2 = self._average_computer() @@ -687,13 +692,11 @@ def _target_tolerance_end(self): target_ntraj = np.max((avg2 / one - (abs(avg) ** 2) / (one ** 2)) / target**2 + 1) - self._estimated_ntraj = min(target_ntraj, self._target_ntraj) - if (self._estimated_ntraj - self._num_rel_trajectories) <= 0: - if (self._estimated_ntraj - self._target_ntraj) < 0: - self.stats["end_condition"] = "target tolerance reached" - else: - self.stats["end_condition"] = "ntraj reached" - return self._estimated_ntraj - self._num_rel_trajectories + self._estimated_ntraj = min(target_ntraj - self._num_rel_trajectories, + self._target_ntraj - self.num_trajectories) + if self._estimated_ntraj <= 0: + self.stats["end_condition"] = "target tolerance reached" + return self._estimated_ntraj def _post_init(self): self._target_ntraj = None From 4f10ca7852e9ff22db572a6cb8ac86f3156bc513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Thu, 18 Apr 2024 16:49:42 -0400 Subject: [PATCH 108/305] Apply suggestions from code review Co-authored-by: Paul --- qutip/solver/stochastic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 9f745920a3..bac0365f0e 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -685,8 +685,9 @@ def run_from_experiment( noise = dW/dt * dW_factors + expect(sc_ops[i] + sc_ops[i].dag, state_t) - Note that the expectation value is usally computed at the start of - the step. Only available for limited integration methods. + Note that this function expects the expectation values to be taken at the start of + the time step, corresponding to the "start" setting for the "store_measurements" option. + Only available for limited integration methods. Returns ------- From 011a74d9468bea080e290d68636d7614d18561a5 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 18 Apr 2024 17:04:32 -0400 Subject: [PATCH 109/305] improve docstring --- qutip/solver/stochastic.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 9f745920a3..5b226a45c4 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -680,10 +680,19 @@ def run_from_experiment( measurement : bool, default : False Whether the passed noise is the Wiener increments ``dW`` (gaussian - noise with standard derivation of dt**0.5), or the measurement:: + noise with standard derivation of dt**0.5), or the measurement. - noise = dW/dt * dW_factors - + expect(sc_ops[i] + sc_ops[i].dag, state_t) + Homodyne measurement is:: + + noise[i][t] = dW/dt + expect(sc_ops[i] + sc_ops[i].dag, state[t]) + + Heterodyne measurement is:: + + noise[i][0][t] = dW/dt * 2**0.5 + + expect(sc_ops[i] + sc_ops[i].dag, state[t]) + + noise[i][1][t] = dW/dt * 2**0.5 + -1j * expect(sc_ops[i] - sc_ops[i].dag, state[t]) Note that the expectation value is usally computed at the start of the step. Only available for limited integration methods. From bc0d13c7884394223d366627bf0e1d826fde5c41 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 19 Apr 2024 10:15:12 -0400 Subject: [PATCH 110/305] small improvements --- qutip/solver/sode/_noise.py | 4 ++-- qutip/tests/solver/test_stochastic.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index c51c63e30f..da71f9b395 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -26,7 +26,7 @@ def _extend(self, idx): def dW(self, t, N): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. - idx0 = int((t - self.t0 + self.dt * 0.4999) // self.dt) + idx0 = round((t-t0) / dt) if idx0 + N - 1 >= self.noise.shape[0]: self._extend(idx0 + N) return self.noise[idx0:idx0 + N, :, :] @@ -40,7 +40,7 @@ def __call__(self, t): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. - idx = int((t - self.t0 + self.dt * 0.4999) // self.dt) + idx = round((t-t0) / dt) if idx >= self.noise.shape[0]: self._extend(idx + 1) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 16706f5cab..ebc6c33e8f 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -423,7 +423,7 @@ def test_run_from_experiment_close(method, heterodyne): H = num(N) a = destroy(N) - sc_ops = [a, a.dag() * 0.1] + sc_ops = [a, a @ a + (a @ a).dag()] psi0 = basis(N, N-1) tlist = np.linspace(0, 0.1, 101) options = { From ff4608970f9f84bcae6736631211592267259e14 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 19 Apr 2024 14:51:17 -0400 Subject: [PATCH 111/305] Add types to more functions --- qutip/solver/krylovsolve.py | 13 ++++++-- qutip/solver/mcsolve.py | 59 +++++++++++++++++++++++++++++-------- qutip/solver/multitraj.py | 20 ++++++++++--- qutip/solver/propagator.py | 28 ++++++++++++++---- qutip/solver/result.py | 2 +- qutip/typing.py | 6 ++-- 6 files changed, 99 insertions(+), 29 deletions(-) diff --git a/qutip/solver/krylovsolve.py b/qutip/solver/krylovsolve.py index 6143aae06c..6e3b51d143 100644 --- a/qutip/solver/krylovsolve.py +++ b/qutip/solver/krylovsolve.py @@ -1,12 +1,19 @@ __all__ = ['krylovsolve'] -from .. import QobjEvo +from .. import QobjEvo, Qobj from .sesolve import SESolver +from numpy.typing import ArrayLike def krylovsolve( - H, psi0, tlist, krylov_dim, e_ops=None, args=None, options=None -): + H: Qobj, + psi0: Qobj, + tlist: ArrayLike, + krylov_dim: int, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + args: dict[str, Any] = None, + options: dict[str, Any] = None, +) -> Result: """ Schrodinger equation evolution of a state vector for time independent Hamiltonians using Krylov method. diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 66005e1080..78d11c6f4f 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -1,6 +1,8 @@ __all__ = ['mcsolve', "MCSolver"] import numpy as np +from numpy.typing import ArrayLike +from numpy.random import SeedSequence from ..core import QobjEvo, spre, spost, Qobj, unstack_columns from .multitraj import MultiTrajSolver, _MTSystem from .solver_base import Solver, Integrator, _solver_deprecation @@ -11,9 +13,21 @@ from time import time -def mcsolve(H, state, tlist, c_ops=(), e_ops=None, ntraj=500, *, - args=None, options=None, seeds=None, target_tol=None, timeout=None, - **kwargs): +def mcsolve( + H: QobjEvoLike, + state: Qobj, + tlist: ArrayLike, + c_ops: QobjEvoLike | list[QobjEvoLike] = (), + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + ntraj: int = 500, + *, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + seeds: int | SeedSequence | list[int | SeedSequence] = None, + target_tol: float = None, + timeout: float = None, + **kwargs, +) -> Result: r""" Monte Carlo evolution of a state vector :math:`|\psi \rangle` for a given Hamiltonian and sets of collapse operators. Options for the @@ -429,7 +443,13 @@ class MCSolver(MultiTrajSolver): "improved_sampling": False, } - def __init__(self, H, c_ops, *, options=None): + def __init__( + self, + H: QobjEvoLike, + c_ops: QobjEvoLike | list[QobjEvoLike], + *, + options: dict[str, Any] = None, + ): _time_start = time() if isinstance(c_ops, (Qobj, QobjEvo)): @@ -502,8 +522,18 @@ def _run_one_traj(self, seed, state, tlist, e_ops, no_jump=False, result.collapse = self._integrator.collapses return seed, result - def run(self, state, tlist, ntraj=1, *, - args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): + def run( + self, + state: Qobj, + tlist: ArrayLike, + ntraj: int = 1, + *, + args: dict[str, Any] = None, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + target_tol: float = None, + timeout: float = None, + seeds: int | SeedSequence | list[int | SeedSequence] = None, + ) -> Result: """ Do the evolution of the Quantum system. See the overridden method for further details. The modification @@ -572,7 +602,7 @@ def _resultclass(self): return McResult @property - def options(self): + def options(self) -> dict: """ Options for monte carlo solver: @@ -640,7 +670,7 @@ def options(self): return self._options @options.setter - def options(self, new_options): + def options(self, new_options: dict[str, Any]): MultiTrajSolver.options.fset(self, new_options) @classmethod @@ -653,7 +683,7 @@ def avail_integrators(cls): } @classmethod - def CollapseFeedback(cls, default=None): + def CollapseFeedback(cls, default: list = None): """ Collapse of the trajectory argument for time dependent systems. @@ -671,14 +701,19 @@ def CollapseFeedback(cls, default=None): Parameters ---------- - default : callable, default : [] - Default function used outside the solver. + default : list, default : [] + Argument value to use outside of solver. """ return _CollapseFeedback(default) @classmethod - def StateFeedback(cls, default=None, raw_data=False, open=False): + def StateFeedback( + cls, + default: Qobj | _data.Data = None, + raw_data: bool = False, + prop: bool = False + ): """ State of the evolution to be used in a time-dependent operator. diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 3ca7930a96..a89f8bc80b 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -83,7 +83,7 @@ def __init__(self, rhs, *, options=None): self._state_metadata = {} self.stats = self._initialize_stats() - def start(self, state, t0, seed=None): + def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence=None): """ Set the initial state and time for a step evolution. @@ -108,7 +108,9 @@ def start(self, state, t0, seed=None): generator = self._get_generator(seeds[0]) self._integrator.set_state(t0, self._prepare_state(state), generator) - def step(self, t, *, args=None, copy=True): + def step( + self, t | Number, *, args: dict[str, Any] = None, copy: bool = True + ) -> Qobj: """ Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. @@ -152,8 +154,18 @@ def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), stats['preparation time'] += time() - start_time return stats, seeds, result, map_func, map_kw, state0 - def run(self, state, tlist, ntraj=1, *, - args=None, e_ops=(), timeout=None, target_tol=None, seeds=None): + def run( + self, + state: Qobj, + tlist: ArrayLike, + ntraj: int = 1, + *, + args: dict[str, Any] = None, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + target_tol: float = None, + timeout: float = None, + seeds: int | SeedSequence | list[int | SeedSequence] = None, + ) -> Result: """ Do the evolution of the Quantum system. diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index f64bf377d0..1ade2a6c54 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -5,6 +5,7 @@ from .. import Qobj, qeye, qeye_like, unstack_columns, QobjEvo, liouvillian from ..core import data as _data +from ..typing import QobjEvoLike from .mesolve import mesolve, MESolver from .sesolve import sesolve, SESolver from .heom.bofin_solvers import HEOMSolver @@ -12,7 +13,14 @@ from .multitraj import MultiTrajSolver -def propagator(H, t, c_ops=(), args=None, options=None, **kwargs): +def propagator( + H: QobjEvoLike, + t: Number, + c_ops: QobjEvoLike | list[QobjEvoLike] = None, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + **kwargs, +) -> Qobj | list[Qobj]: r""" Calculate the propagator U(t) for the density matrix or wave function such that :math:`\psi(t) = U(t)\psi(0)` or @@ -77,7 +85,7 @@ def propagator(H, t, c_ops=(), args=None, options=None, **kwargs): return out[-1] -def propagator_steadystate(U): +def propagator_steadystate(U: Qobj) -> Qobj: r"""Find the steady state for successive applications of the propagator :math:`U`. @@ -154,8 +162,16 @@ class Propagator: U = QobjEvo(Propagator(H)) """ - def __init__(self, system, *, c_ops=(), args=None, options=None, - memoize=10, tol=1e-14): + def __init__( + self, + system: Qobj | QobjEvo | Solver, + *, + c_ops: QobjEvoLike | list[QobjEvoLike] = None, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + memoize: int = 10, + tol: float = 1e-14, + ): if isinstance(system, MultiTrajSolver): raise TypeError("Non-deterministic solvers cannot be used " "as a propagator system") @@ -199,7 +215,7 @@ def _lookup_or_compute(self, t): self._insert(t, U, idx) return U - def __call__(self, t, t_start=0, **args): + def __call__(self, t: float, t_start: float = 0, **args): """ Get the propagator from ``t_start`` to ``t``. @@ -235,7 +251,7 @@ def __call__(self, t, t_start=0, **args): U = self._lookup_or_compute(t) return U - def inv(self, t, **args): + def inv(self, t: float, **args): """ Get the inverse of the propagator at ``t``, such that ``psi_0 = U.inv(t) @ psi_t`` diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 7612441efb..72b8c5b860 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -225,7 +225,7 @@ class Result(_BaseResult): def __init__( self, - e_ops, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, options: ResultOptions, *, solver: str = None, diff --git a/qutip/typing.py b/qutip/typing.py index f5d86757c2..1972910a66 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -1,7 +1,4 @@ from typing import Sequence, Union, Any, Callable, Protocol - -# from .core.cy.qobjEvo import QobjEvoLike, Element -# from .core.coeffients import CoefficientLike from numbers import Number, Real import numpy as np import scipy.interpolate @@ -30,8 +27,11 @@ def __call__(self, t: Real, **kwargs) -> Number: Any, ] + ElementType = Union[QEvoProtocol, "Qobj", tuple["Qobj", CoefficientLike]] + QobjEvoLike = Union["Qobj", "QobjEvo", ElementType, Sequence[ElementType]] + LayerType = Union[str, type] From eb651eff60a2fb491df9b243c94fd833b75ebdfa Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 19 Apr 2024 16:03:05 -0400 Subject: [PATCH 112/305] Add entry in contributing documentation --- doc/development/contributing.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 083754475e..3b376715e7 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -113,6 +113,20 @@ This includes using the same variable names, especially if they are function arg Other than this, general "good-practice" Python standards apply: try not to duplicate code; try to keep functions short, descriptively-named and side-effect free; provide a docstring for every new function; and so on. +Type Hints +---------- + +Adding type hints to users facing functions is recommended. +QuTiP's approach is such: + +- Type hints are *hints* for the users. +- Type hints can show the preferred usage over real implementation, for example: + - ``Qobj.__mul__`` is typed to support product with scalar, not other ``Qobj``, for which ``__matmul__`` should is preferred. + - ``solver.options`` claim it return a dict not ``_SolverOptions`` (which is a subclass of dict). +- Type alias are added to ``qutip.typing``. +- `Any` can be used for input which type can be extended by plugin modules, (``qutip-cupy``, ``qutip-jax``, etc.) + + Documenting ----------- From ce01e7fb206eeadd5d2cc8a8a628fb6f3dd77e31 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 19 Apr 2024 17:05:12 -0400 Subject: [PATCH 113/305] PPoly and Bspline accept real coeff + tested --- qutip/core/cy/coefficient.pyx | 4 ++-- qutip/tests/core/test_coefficient.py | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/qutip/core/cy/coefficient.pyx b/qutip/core/cy/coefficient.pyx index 42a84cfca6..3df6eeca60 100644 --- a/qutip/core/cy/coefficient.pyx +++ b/qutip/core/cy/coefficient.pyx @@ -517,7 +517,7 @@ cdef class InterCoefficient(Coefficient): @classmethod def from_PPoly(cls, ppoly, **_): - return cls.restore(ppoly.x, ppoly.c) + return cls.restore(ppoly.x, np.array(ppoly.c, complex, copy=False)) @classmethod def from_Bspline(cls, spline, **_): @@ -528,7 +528,7 @@ cdef class InterCoefficient(Coefficient): poly = np.concatenate([ spline(tlist, i) / fact[i] for i in range(spline.k, -1, -1) - ]).reshape((spline.k+1, -1)) + ]).reshape((spline.k+1, -1)).astype(complex, copy=False) return cls.restore(tlist, poly) cpdef Coefficient copy(self): diff --git a/qutip/tests/core/test_coefficient.py b/qutip/tests/core/test_coefficient.py index bb5204c611..3fb92a2bc7 100644 --- a/qutip/tests/core/test_coefficient.py +++ b/qutip/tests/core/test_coefficient.py @@ -381,9 +381,13 @@ def test_CoeffArray(order): assert derrs[i] == pytest.approx(0.0, abs=0.0001) -def test_CoeffFromScipy(): +@pytest.mark.parametrize('imag', [True, False]) +def test_CoeffFromScipyPPoly(imag): tlist = np.linspace(0, 1.01, 101) - y = np.exp((-1 + 1j) * tlist) + if imag: + y = np.exp(-1j * tlist) + else: + y = np.exp(-1 * tlist) coeff = coefficient(y, tlist=tlist, order=3) from_scipy = coefficient(interp.CubicSpline(tlist, y)) @@ -398,6 +402,24 @@ def test_CoeffFromScipy(): _assert_eq_over_interval(coeff, from_scipy, rtol=1e-8, inside=True) +@pytest.mark.parametrize('imag', [True, False]) +def test_CoeffFromScipyBSpline(imag): + tlist = np.linspace(-0.1, 1.1, 121) + if imag: + y = np.exp(-1j * tlist) + else: + y = np.exp(-1 * tlist) + + spline = interp.BSpline(tlist, y, 2) + + def func(t): + return complex(spline(t)) + + coverted = coefficient(spline) + raw_scipy = coefficient(func) + _assert_eq_over_interval(coverted, raw_scipy, rtol=1e-8, inside=True) + + @pytest.mark.parametrize('map_func', [ pytest.param(qutip.solver.parallel.parallel_map, id='parallel_map'), pytest.param(qutip.solver.parallel.loky_pmap, id='loky_pmap'), From d4103b2c7cc25ccb4a25d05b1a442099b417bfab Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Apr 2024 09:05:14 -0400 Subject: [PATCH 114/305] Codeclimat --- qutip/solver/sode/_noise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/solver/sode/_noise.py b/qutip/solver/sode/_noise.py index f9c5b12c94..eec07a1485 100644 --- a/qutip/solver/sode/_noise.py +++ b/qutip/solver/sode/_noise.py @@ -26,7 +26,7 @@ def _extend(self, idx): def dW(self, t, N): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. - idx0 = round((t- self.t0) / self.dt) + idx0 = round((t - self.t0) / self.dt) if idx0 + N - 1 >= self.noise.shape[0]: self._extend(idx0 + N) return self.noise[idx0:idx0 + N, :, :] @@ -40,7 +40,7 @@ def __call__(self, t): # Find the index of t. # Rounded to the closest step, but only multiple of dt are expected. - idx = round((t- self.t0) / self.dt) + idx = round((t - self.t0) / self.dt) if idx >= self.noise.shape[0]: self._extend(idx + 1) From cc389c60927f10a46506d4b4e5eccb74165a5338 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Apr 2024 10:08:43 -0400 Subject: [PATCH 115/305] Increase test stability --- qutip/tests/solver/test_stochastic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index ebc6c33e8f..eaf2f9c015 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -425,7 +425,7 @@ def test_run_from_experiment_close(method, heterodyne): a = destroy(N) sc_ops = [a, a @ a + (a @ a).dag()] psi0 = basis(N, N-1) - tlist = np.linspace(0, 0.1, 101) + tlist = np.linspace(0, 0.1, 251) options = { "store_measurement": "start", "dt": tlist[1], @@ -470,10 +470,10 @@ def test_run_from_experiment_open(method, heterodyne): a = destroy(N) sc_ops = [a, a.dag() * 0.1] psi0 = basis(N, N-1) - tlist = np.linspace(0, 1, 101) + tlist = np.linspace(0, 1, 251) options = { "store_measurement": "start", - "dt": 0.01, + "dt": tlist[1], "store_states": True, "method": method, } From 2bf11c29c440e4e38a61cd4200bb09ef0775f328 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Apr 2024 10:49:42 -0400 Subject: [PATCH 116/305] Improve propagator doctring for 't' --- qutip/solver/propagator.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index f64bf377d0..01fc442182 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -28,7 +28,11 @@ def propagator(H, t, c_ops=(), args=None, options=None, **kwargs): that can be made into :obj:`.QobjEvo` are also accepted. t : float or array-like - Time or list of times for which to evaluate the propagator. + Time or list of times for which to evaluate the propagator. If a single + time ``t`` is passed, the propagator from ``0`` to ``t`` is computed. + When ``t`` is a list, the propagators from the first time in the list + to each elements in ``t`` is returned. In that case, the first output + will always be the identity matrix. c_ops : list, optional List of Qobj or QobjEvo collapse operators. From c11629839f1c63c1545b8efa13535912383ec6a6 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Apr 2024 11:17:35 -0400 Subject: [PATCH 117/305] Update the release guide to use readthedocs instead of hosting docs in qutip.gitub.io --- doc/development/release_distribution.rst | 45 +++--------------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/doc/development/release_distribution.rst b/doc/development/release_distribution.rst index 75c581cbf9..9419a87faf 100644 --- a/doc/development/release_distribution.rst +++ b/doc/development/release_distribution.rst @@ -120,6 +120,8 @@ You should now have a branch that you can see on the GitHub website that is call If you notice you have made a mistake, you can make additional pull requests to the release branch to fix it. ``master`` should look pretty similar, except the ``VERSION`` will be higher and have a ``.dev`` suffix, and the "Development Status" in ``setup.cfg`` will be different. +* Activate the readthedocs build for the newly created version branch and set it as the latest. + You are now ready to actually perform the release. Go to deploy_. @@ -189,7 +191,7 @@ Go to the `"Actions" tab at the top of the QuTiP code repository ..pdf`` into the folder ``downloads/..``. - -The legacy html documentation should be in a subfolder like :: - - docs/. - -For a major or minor release the previous version documentation should be moved into this folder. - -The latest version HTML documentation should be the folder :: - - docs/latest - -For any release which new documentation is included -- copy the contents ``qutip/doc/_build/html`` into this folder. **Note that the underscores at start of the subfolder names will need to be removed, otherwise Jekyll will ignore the folders**. There is a script in the ``docs`` folder for this. -https://github.com/qutip/qutip.github.io/blob/master/docs/remove_leading_underscores.py - - HTML File Updates ----------------- @@ -317,7 +282,7 @@ HTML File Updates - Edit ``documentation.html`` - * The previous release tags should be moved (copied) to the 'Previous releases' section. + * For major and minor release, the previous release tags should be moved (copied) to the 'Previous releases' section and the links to the readthedocs of the new version added the to 'Latest releases' section. .. _cforge: From c068b35f1505651c1b94fb94aca80949166bb35e Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Apr 2024 12:05:43 -0400 Subject: [PATCH 118/305] Fix broken link --- doc/development/release_distribution.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/development/release_distribution.rst b/doc/development/release_distribution.rst index 9419a87faf..c7831e602e 100644 --- a/doc/development/release_distribution.rst +++ b/doc/development/release_distribution.rst @@ -16,10 +16,9 @@ In short, the steps you need to take are: 1. Prepare the release branch (see git_). 2. Run the "Build wheels, optionally deploy to PyPI" GitHub action to build binary and source packages and upload them to PyPI (see deploy_). -3. Retrieve the built documentation from GitHub (see docbuild_). -4. Create a GitHub release and uploaded the built files to it (see github_). -5. Update `qutip.org `_ with the new links and documentation (web_). -6. Update the conda feedstock, deploying the package to ``conda`` (cforge_). +3. Create a GitHub release and uploaded the built files to it (see github_). +4. Update `qutip.org `_ with the new links and documentation (web_). +5. Update the conda feedstock, deploying the package to ``conda`` (cforge_). From d089bd812d950ab5e74a74039ed885c48b2fa456 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Apr 2024 12:22:21 -0400 Subject: [PATCH 119/305] Reword sidebar update instructions --- doc/development/release_distribution.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/development/release_distribution.rst b/doc/development/release_distribution.rst index c7831e602e..0f923a927b 100644 --- a/doc/development/release_distribution.rst +++ b/doc/development/release_distribution.rst @@ -276,8 +276,8 @@ HTML File Updates - Edit ``_includes/sidebar.html`` - * The 'Latest release' version should be updated. The gztar and zip file links will need the micro release number updating in the traceEvent and file name. - * The link to the documentation folder and PDF file (if created) should be updated. + * Add the new version and release date. Only actively developed version should be listed. Micro replace the previous entry but the last major can be kept. + * Link to the installation instruction, documentation, source code and changelog should be updated. - Edit ``documentation.html`` From aa70784d1ce3d346dcfe4bf4c5a1e107d7c2a935 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Apr 2024 08:56:49 -0400 Subject: [PATCH 120/305] Complete settings page in guide --- doc/apidoc/classes.rst | 19 +++++- doc/guide/guide-settings.rst | 129 ++++++++++++++++++++++++++++++++++- qutip/core/coefficient.py | 30 +++++--- qutip/core/options.py | 5 -- qutip/settings.py | 28 +------- 5 files changed, 164 insertions(+), 47 deletions(-) diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index ecd0cd7a97..f831adc7ab 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -7,7 +7,7 @@ Classes .. _classes-qobj: Qobj --------------- +---- .. autoclass:: qutip.core.qobj.Qobj :members: @@ -16,17 +16,24 @@ Qobj .. _classes-qobjevo: QobjEvo --------------- +------- .. autoclass:: qutip.core.cy.qobjevo.QobjEvo :members: :special-members: __call__ +.. _classes-coreoptions: + +CoreOptions +----------- + +.. autoclass:: qutip.core.options.CoreOptions + .. _classes-bloch: Bloch sphere ---------------- +------------ .. autoclass:: qutip.bloch.Bloch :members: @@ -265,6 +272,12 @@ Distribution functions .. autoclass:: qutip.distributions.Distribution :members: +CompilationOptions +------------------ + +.. autoclass:: qutip.core.coefficient.CompilationOptions + + .. Docstrings are empty... diff --git a/doc/guide/guide-settings.rst b/doc/guide/guide-settings.rst index 7314ff3484..7ffe7baa1e 100644 --- a/doc/guide/guide-settings.rst +++ b/doc/guide/guide-settings.rst @@ -1,5 +1,66 @@ .. _settings: +************** +QuTiP settings +************** + +QuTiP has multiple settings that control it's behaviour: + +* ``qutip.settings`` contain installation and runtime information. + Most of these parameters are readonly. But systems paths used by QuTiP are + also included here and could need updating in none standard environment. +* ``qutip.settings.core`` contains options for operations with ``Qobj`` and + other qutip's class. All options are writable. +* ``qutip.settings.compile`` has options that control compilation of string + coefficients to cython modules. All options are writable. + +.. _settings-install: + +******************** +Environment settings +******************** + +``qutip.settings`` has information about the run time environment: + +.. tabularcolumns:: | p{3cm} | p{2cm} | p{10cm} | + +.. cssclass:: table-striped + ++-------------------+-----------+----------------------------------------------------------+ +| Setting | Read Only | Description | ++===================+===========+==========================================================+ +| `has_mkl` | True | Whether qutip can find mkl libraries. | +| | | mkl sparse linear equation solver can be used when True. | ++-------------------+-----------+----------------------------------------------------------+ +| `mkl_lib` | True | Path of the mkl libraries found. | ++-------------------+-----------+----------------------------------------------------------+ +| `ipython` | True | Whether running in ipython. | ++-------------------+-----------+----------------------------------------------------------+ +| `eigh_unsafe` | True | Whether to use eig for hermitian matrix since it can | +| | | segfault in some conditions. | ++-------------------+-----------+----------------------------------------------------------+ +| `coeffroot` | False | Directory in which QuTiP creates cython module for | +| | | string coefficient. | ++-------------------+-----------+----------------------------------------------------------+ +| `coeff_write_ok` | True | Whether QuTiP has write permission for `coeffroot`. | ++-------------------+-----------+----------------------------------------------------------+ +| `idxint_size` | True | Whether QuTiP's sparse matrix indices use 32 or 64 bits. | +| | | Sparse matrices' size are limited to 2**(idxint_size-1). | ++-------------------+-----------+----------------------------------------------------------+ +| `num_cpus` | True | Detected number of cpus. | ++-------------------+-----------+----------------------------------------------------------+ +| `colorblind_safe` | False | Control the default cmap in visualization functions. | ++-------------------+-----------+----------------------------------------------------------+ + + +It may be needed to update ``coeffroot`` if the default HOME is not writable. It can be done with: + +>>> qutip.settings.coeffroot = "path/to/string/coeff/directory" + +New to version 5, string compiled in a session are kept for future sessions. +As long as the same ``coeffroot`` is used, each string will only be compiled once. + + ********************************* Modifying Internal QuTiP Settings ********************************* @@ -9,7 +70,7 @@ Modifying Internal QuTiP Settings User Accessible Parameters ========================== -In this section we show how to modify a few of the internal parameters used by QuTiP. +In this section we show how to modify a few of the internal parameters used by ``Qobj``. The settings that can be modified are given in the following table: .. tabularcolumns:: | p{3cm} | p{5cm} | p{5cm} | @@ -19,23 +80,32 @@ The settings that can be modified are given in the following table: +------------------------------+----------------------------------------------+------------------------------+ | Setting | Description | Options | +==============================+==============================================+==============================+ -| `auto_tidyup` | Automatically tidyup sparse quantum objects. | True / False | +| `auto_tidyup` | Automatically tidyup sparse quantum objects. | bool {True} | +------------------------------+----------------------------------------------+------------------------------+ | `auto_tidyup_atol` | Tolerance used by tidyup. (sparse only) | float {1e-14} | +------------------------------+----------------------------------------------+------------------------------+ +| `auto_tidyup_dims` | Whether the scalar dimension are contracted | bool {False} | ++------------------------------+----------------------------------------------+------------------------------+ | `atol` | General absolute tolerance. | float {1e-12} | +------------------------------+----------------------------------------------+------------------------------+ | `rtol` | General relative tolerance. | float {1e-12} | +------------------------------+----------------------------------------------+------------------------------+ | `function_coefficient_style` | Signature expected by function coefficients. | {"auto", "pythonic", "dict"} | +------------------------------+----------------------------------------------+------------------------------+ +| `default_dtype` | Data format used when creating Qobj from | {None, "CSR", "Dense", | +| | QuTiP functions, such as ``qeye``. | "Dia"} + other from plugins | ++------------------------------+----------------------------------------------+------------------------------+ + +See :class:`.CoreOptions`. .. _settings-usage: Example: Changing Settings ========================== -The two most important settings are ``auto_tidyup`` and ``auto_tidyup_atol`` as they control whether the small elements of a quantum object should be removed, and what number should be considered as the cut-off tolerance. +The two most important settings are ``auto_tidyup`` and ``auto_tidyup_atol`` as +they control whether the small elements of a quantum object should be removed, +and what number should be considered as the cut-off tolerance. Modifying these, or any other parameters, is quite simple:: >>> qutip.settings.core["auto_tidyup"] = False @@ -44,3 +114,56 @@ The settings can also be changed for a code block:: >>> with qutip.CoreOptions(atol=1e-5): >>> assert qutip.qeye(2) * 1e-9 == qutip.qzero(2) + + + +.. _settings-compile: + +String Coefficient Parameters +============================= + +String based coefficient used for time dependent system are compiled using Cython when available. +Speeding the simulations, it tries to set c types to passed variables. +``qutip.settings.compile`` has multiple options for compilation. + +There are options are about to whether to compile. + +- | use_cython: bool + | Whether to compile string using cython or using ``eval``. +- | recompile: bool [False] + | Whether to force recompilation or use a previously constructed coefficient if available. + + +Some options passed to cython and the compiler (for advanced user). + +- | compiler_flags: str + | C++ compiler flags. +- | link_flags: str + | C++ link flags. +- | build_dir: str + | cythonize's build_dir. +- | extra_import: str + | import or cimport line of code to add to the cython file. +- | clean_on_error: bool [True] + | Whether to erase the created file if compilation failed. + + +Lastly some options control how qutip tries to detect C types (for advanced user). + +- | try_parse: bool [True] + | Whether qutip parse the string to detect common patterns. + | When True, "cos(w * t)" and "cos(a * t)" will use the same compiled coefficient. +- | static_types: bool [True] + | If False, every variable will be typed as ``object``, (except ``t`` which is double). + | If True, scalar (int, float, complex), string and Data types are detected. +- | accept_int: bool [None] + | Whether to type ``args`` values which are python int as int or float/complex. + | Per default it is True when subscription (``a[i]``) or comparison (``a > b``) are used. +- | accept_float: bool [None] + | Whether to type ``args`` values which are python float as float or complex. + | Per default it is True when subscription (``a[i]``) or comparison (``a > b``) are used. + + +These options can be set at a global level in ``qutip.settings.compile`` or by passing a :class:`.CompilationOptions` instance to the `.coefficient` functions. + +>>> qutip.coefficient("cos(t)", compile_opt=CompilationOptions(recompile=True)) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index 11c87e2f57..3e9db45310 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -267,6 +267,7 @@ class CompilationOptions(QutipOptions): try: import cython import filelock + import setuptools _use_cython = True except ImportError: _use_cython = False @@ -277,7 +278,7 @@ class CompilationOptions(QutipOptions): "try_parse": True, "static_types": True, "accept_int": None, - "accept_float": True, + "accept_float": None, "recompile": False, "compiler_flags": _compiler_flags, "link_flags": _link_flags, @@ -293,7 +294,7 @@ class CompilationOptions(QutipOptions): # Version number of the Coefficient -COEFF_VERSION = "1.1" +COEFF_VERSION = "1.2" try: root = os.path.join(qset.tmproot, f"qutip_coeffs_{COEFF_VERSION}") @@ -309,15 +310,17 @@ def clean_compiled_coefficient(all=False): Parameter: ---------- all: bool - If not `all` will remove only previous version. + If not `all`, it will remove only previous version. """ import glob import shutil tmproot = qset.tmproot - root = os.path.join(tmproot, f'qutip_coeffs_{COEFF_VERSION}') + active = qset.coeffroot folders = glob.glob(os.path.join(tmproot, 'qutip_coeffs_') + "*") + if all: + shutil.rmtree(active) for folder in folders: - if all or folder != root: + if folder != active: shutil.rmtree(folder) # Recreate the empty folder. qset.coeffroot = qset.coeffroot @@ -384,8 +387,8 @@ def coeff_from_str(base, args, args_ctypes, compile_opt=None, **_): if not compile_opt['use_cython']: if WARN_MISSING_MODULE[0]: warnings.warn( - "Both `cython` and `filelock` are required for compilation of " - "string coefficents. Falling back on `eval`.") + "`cython` `setuptools` and `filelock` are required for " + "compilation of string coefficents. Falling back on `eval`.") # Only warns once. WARN_MISSING_MODULE[0] = 0 return StrFunctionCoefficient(base, args) @@ -711,8 +714,17 @@ def parse(code, args, compile_opt): accept_float = compile_opt['accept_float'] if accept_int is None: # If there is a subscript: a[b] int are always accepted to be safe - # with TypeError - accept_int = "SUBSCR" in dis.Bytecode(code).dis() + # with TypeError. + # Also comparison is not supported for complex. + accept_int = ( + "SUBSCR" in dis.Bytecode(code).dis() + or "COMPARE_OP" in dis.Bytecode(code).dis() + ) + if accept_float is None: + accept_float = ( + "SUBSCR" in dis.Bytecode(code).dis() + or "COMPARE_OP" in dis.Bytecode(code).dis() + ) for word in code.split(): if word not in names: # syntax diff --git a/qutip/core/options.py b/qutip/core/options.py index 4748dd3612..ae5747629b 100644 --- a/qutip/core/options.py +++ b/qutip/core/options.py @@ -70,9 +70,6 @@ class CoreOptions(QutipOptions): With auto_tidyup_dims: ``basis([2, 2]).dims == [[2, 2], [1]]`` - auto_herm : boolTrue - detect hermiticity - atol : float {1e-12} General absolute tolerance @@ -112,8 +109,6 @@ class CoreOptions(QutipOptions): "auto_tidyup": True, # use auto tidyup dims on multiplication "auto_tidyup_dims": False, - # detect hermiticity - "auto_herm": True, # general absolute tolerance "atol": 1e-12, # general relative tolerance diff --git a/qutip/settings.py b/qutip/settings.py index 29ee053b75..bcd9ee09a9 100644 --- a/qutip/settings.py +++ b/qutip/settings.py @@ -212,7 +212,7 @@ def coeff_write_ok(self): return os.access(self.coeffroot, os.W_OK) @property - def has_openmp(self): + def _has_openmp(self): return False # We keep this as a reminder for when openmp is restored: see Pull #652 # os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' @@ -240,32 +240,6 @@ def num_cpus(self): os.environ['QUTIP_NUM_PROCESSES'] = str(num_cpus) return num_cpus - @property - def debug(self): - """ - Debug mode for development. - """ - return self._debug - - @debug.setter - def debug(self, value): - self._debug = value - - @property - def log_handler(self): - """ - Define whether log handler should be: - - default: switch based on IPython detection - - stream: set up non-propagating StreamHandler - - basic: call basicConfig - - null: leave logging to the user - """ - return self._log_handler - - @log_handler.setter - def log_handler(self, value): - self._log_handler = value - @property def colorblind_safe(self): """ From 09fe456a5eaaa73eb64ff6209cb95946a567ba79 Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:46:39 +0800 Subject: [PATCH 121/305] Fix mistake in doc --- doc/guide/guide-basics.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index 1d17cf0976..73e946881a 100644 --- a/doc/guide/guide-basics.rst +++ b/doc/guide/guide-basics.rst @@ -349,7 +349,7 @@ Conversion between storage type is done using the :meth:`.Qobj.to` method. >>> q.to("CSR").data CSR(shape=(4, 4), nnz=3) - >>> q.to("CSR").data_as("CSR_matrix") + >>> q.to("CSR").data_as("csr_matrix") <4x4 sparse matrix of type '' with 3 stored elements in Compressed Sparse Row format> From 4403a97179ee00152b8f0d4c5c75fe1ef7ebe0fc Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Apr 2024 11:38:36 -0400 Subject: [PATCH 122/305] Use table for CompileOptions too. --- doc/apidoc/classes.rst | 1 + doc/guide/guide-settings.rst | 124 ++++++++++++++++----------- qutip/core/coefficient.py | 32 ++++--- qutip/core/options.py | 12 ++- qutip/tests/core/test_coefficient.py | 10 ++- 5 files changed, 112 insertions(+), 67 deletions(-) diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index f831adc7ab..05eb079952 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -28,6 +28,7 @@ CoreOptions ----------- .. autoclass:: qutip.core.options.CoreOptions + :members: .. _classes-bloch: diff --git a/doc/guide/guide-settings.rst b/doc/guide/guide-settings.rst index 7ffe7baa1e..5f7b176b01 100644 --- a/doc/guide/guide-settings.rst +++ b/doc/guide/guide-settings.rst @@ -77,26 +77,26 @@ The settings that can be modified are given in the following table: .. cssclass:: table-striped -+------------------------------+----------------------------------------------+------------------------------+ -| Setting | Description | Options | -+==============================+==============================================+==============================+ -| `auto_tidyup` | Automatically tidyup sparse quantum objects. | bool {True} | -+------------------------------+----------------------------------------------+------------------------------+ -| `auto_tidyup_atol` | Tolerance used by tidyup. (sparse only) | float {1e-14} | -+------------------------------+----------------------------------------------+------------------------------+ -| `auto_tidyup_dims` | Whether the scalar dimension are contracted | bool {False} | -+------------------------------+----------------------------------------------+------------------------------+ -| `atol` | General absolute tolerance. | float {1e-12} | -+------------------------------+----------------------------------------------+------------------------------+ -| `rtol` | General relative tolerance. | float {1e-12} | -+------------------------------+----------------------------------------------+------------------------------+ -| `function_coefficient_style` | Signature expected by function coefficients. | {"auto", "pythonic", "dict"} | -+------------------------------+----------------------------------------------+------------------------------+ -| `default_dtype` | Data format used when creating Qobj from | {None, "CSR", "Dense", | -| | QuTiP functions, such as ``qeye``. | "Dia"} + other from plugins | -+------------------------------+----------------------------------------------+------------------------------+ - -See :class:`.CoreOptions`. ++------------------------------+----------------------------------------------+--------------------------------+ +| Options | Description | type [default] | ++==============================+==============================================+================================+ +| `auto_tidyup` | Automatically tidyup sparse quantum objects. | bool [True] | ++------------------------------+----------------------------------------------+--------------------------------+ +| `auto_tidyup_atol` | Tolerance used by tidyup. (sparse only) | float [1e-14] | ++------------------------------+----------------------------------------------+--------------------------------+ +| `auto_tidyup_dims` | Whether the scalar dimension are contracted | bool [False] | ++------------------------------+----------------------------------------------+--------------------------------+ +| `atol` | General absolute tolerance. | float [1e-12] | ++------------------------------+----------------------------------------------+--------------------------------+ +| `rtol` | General relative tolerance. | float [1e-12] | ++------------------------------+----------------------------------------------+--------------------------------+ +| `function_coefficient_style` | Signature expected by function coefficients. | {["auto"], "pythonic", "dict"} | ++------------------------------+----------------------------------------------+--------------------------------+ +| `default_dtype` | Data format used when creating Qobj from | {[None], "CSR", "Dense", | +| | QuTiP functions, such as ``qeye``. | "Dia"} + other from plugins | ++------------------------------+----------------------------------------------+--------------------------------+ + +See also :class:`.CoreOptions`. .. _settings-usage: @@ -128,42 +128,68 @@ Speeding the simulations, it tries to set c types to passed variables. There are options are about to whether to compile. -- | use_cython: bool - | Whether to compile string using cython or using ``eval``. -- | recompile: bool [False] - | Whether to force recompilation or use a previously constructed coefficient if available. +.. tabularcolumns:: | p{3cm} | p{10cm} | + +.. cssclass:: table-striped + ++--------------------------+-----------------------------------------------------------+ +| Options | Description | ++==========================+===========================================================+ +| `use_cython` | Whether to compile string using cython or using ``eval``. | ++--------------------------+-----------------------------------------------------------+ +| `recompile` | Whether to force recompilation or use a previously | +| | constructed coefficient if available. | ++--------------------------+-----------------------------------------------------------+ Some options passed to cython and the compiler (for advanced user). -- | compiler_flags: str - | C++ compiler flags. -- | link_flags: str - | C++ link flags. -- | build_dir: str - | cythonize's build_dir. -- | extra_import: str - | import or cimport line of code to add to the cython file. -- | clean_on_error: bool [True] - | Whether to erase the created file if compilation failed. +.. tabularcolumns:: | p{3cm} | p{10cm} | + +.. cssclass:: table-striped + ++--------------------------+-----------------------------------------------------------+ +| Options | Description | ++==========================+===========================================================+ +| `compiler_flags` | C++ compiler flags. | ++--------------------------+-----------------------------------------------------------+ +| `link_flags` | C++ linker flags. | ++--------------------------+-----------------------------------------------------------+ +| `build_dir` | cythonize's build_dir. | ++--------------------------+-----------------------------------------------------------+ +| `extra_import` | import or cimport line of code to add to the cython file. | ++--------------------------+-----------------------------------------------------------+ +| `clean_on_error` | Whether to erase the created file if compilation failed. | ++--------------------------+-----------------------------------------------------------+ Lastly some options control how qutip tries to detect C types (for advanced user). -- | try_parse: bool [True] - | Whether qutip parse the string to detect common patterns. - | When True, "cos(w * t)" and "cos(a * t)" will use the same compiled coefficient. -- | static_types: bool [True] - | If False, every variable will be typed as ``object``, (except ``t`` which is double). - | If True, scalar (int, float, complex), string and Data types are detected. -- | accept_int: bool [None] - | Whether to type ``args`` values which are python int as int or float/complex. - | Per default it is True when subscription (``a[i]``) or comparison (``a > b``) are used. -- | accept_float: bool [None] - | Whether to type ``args`` values which are python float as float or complex. - | Per default it is True when subscription (``a[i]``) or comparison (``a > b``) are used. - - -These options can be set at a global level in ``qutip.settings.compile`` or by passing a :class:`.CompilationOptions` instance to the `.coefficient` functions. +.. tabularcolumns:: | p{3cm} | p{10cm} | + +.. cssclass:: table-striped + ++--------------------------+-----------------------------------------------------------------------------------------+ +| Options | Description | ++==========================+=========================================================================================+ +| `try_parse` | Whether qutip parse the string to detect common patterns. | +| | | +| | When True, "cos(w * t)" and "cos(a * t)" will use the same compiled coefficient. | ++--------------------------+-----------------------------------------------------------------------------------------+ +| `static_types` | If False, every variable will be typed as ``object``, (except ``t`` which is double). | +| | | +| | If True, scalar (int, float, complex), string and Data types are detected. | ++--------------------------+-----------------------------------------------------------------------------------------+ +| `accept_int` | Whether to type ``args`` values which are python int as int or float/complex. | +| | | +| | Per default it is True when subscription (``a[i]``) is used. | ++--------------------------+-----------------------------------------------------------------------------------------+ +| `accept_float` | Whether to type ``args`` values which are python float as int or float/complex. | +| | | +| | Per default it is True when comparison (``a > b``) is used. | ++--------------------------+-----------------------------------------------------------------------------------------+ + + +These options can be set at a global level in ``qutip.settings.compile`` or by passing a :class:`.CompilationOptions` instance to the :func:`.coefficient` functions. >>> qutip.coefficient("cos(t)", compile_opt=CompilationOptions(recompile=True)) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index 3e9db45310..b438442757 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -209,11 +209,30 @@ def const(value): class CompilationOptions(QutipOptions): """ + Options that control compilation of string based coefficient to Cython. + + These options can be set globaly: + + ``settings.compile["compiler_flags"] = "-O1"`` + + In a ``with`` block: + + ``with CompilationOptions(use_cython=False):`` + + Or as an instance: + + ``coefficient(coeff, compile_opt=CompilationOptions(recompile=True))`` + + ******************** Compilation options: + ******************** use_cython: bool Whether to compile strings as cython code or use python's ``exec``. + recompile : bool + Do not use previously made files but build a new one. + try_parse: bool [True] Whether to try parsing the string for reuse and static typing. @@ -229,9 +248,6 @@ class CompilationOptions(QutipOptions): accept_float : bool Whether to use the type ``float`` or upgrade them to ``complex``. - recompile : bool - Do not use previously made files but build a new one. - compiler_flags : str Flags to pass to the compiler, ex: "-Wall -O3"... Flags not matching your comiler and OS may cause compilation to fail. @@ -716,15 +732,9 @@ def parse(code, args, compile_opt): # If there is a subscript: a[b] int are always accepted to be safe # with TypeError. # Also comparison is not supported for complex. - accept_int = ( - "SUBSCR" in dis.Bytecode(code).dis() - or "COMPARE_OP" in dis.Bytecode(code).dis() - ) + accept_int = "SUBSCR" in dis.Bytecode(code).dis() if accept_float is None: - accept_float = ( - "SUBSCR" in dis.Bytecode(code).dis() - or "COMPARE_OP" in dis.Bytecode(code).dis() - ) + accept_float = "COMPARE_OP" in dis.Bytecode(code).dis() for word in code.split(): if word not in names: # syntax diff --git a/qutip/core/options.py b/qutip/core/options.py index ae5747629b..1fa374a0cb 100644 --- a/qutip/core/options.py +++ b/qutip/core/options.py @@ -56,18 +56,24 @@ class CoreOptions(QutipOptions): comparison or coefficient's format. Values can be changed in ``qutip.settings.core`` or by using context: - ``with CoreOptions(atol=1e-6): ...``. - Options - ------- + ``with CoreOptions(atol=1e-6): ...`` + + ******** + Options: + ******** + auto_tidyup : bool Whether to tidyup during sparse operations. auto_tidyup_dims : bool [False] Use auto tidyup dims on multiplication, tensor, etc. Without auto_tidyup_dims: + ``basis([2, 2]).dims == [[2, 2], [1, 1]]`` + With auto_tidyup_dims: + ``basis([2, 2]).dims == [[2, 2], [1]]`` atol : float {1e-12} diff --git a/qutip/tests/core/test_coefficient.py b/qutip/tests/core/test_coefficient.py index bb5204c611..6e35c93b46 100644 --- a/qutip/tests/core/test_coefficient.py +++ b/qutip/tests/core/test_coefficient.py @@ -230,7 +230,7 @@ def test_CoeffOptions(): base = "1 + 1. + 1j" options = [] options.append(CompilationOptions(accept_int=True)) - options.append(CompilationOptions(accept_float=False)) + options.append(CompilationOptions(accept_float=True)) options.append(CompilationOptions(static_types=True)) options.append(CompilationOptions(try_parse=False)) options.append(CompilationOptions(use_cython=False)) @@ -244,10 +244,12 @@ def test_CoeffOptions(): def test_warn_no_cython(): option = CompilationOptions(use_cython=False) WARN_MISSING_MODULE[0] = 1 - with pytest.warns( - UserWarning, match="`cython` and `filelock` are required" - ): + with pytest.warns(UserWarning) as warning: coefficient("t", compile_opt=option) + assert all( + module in warning[0].message.args[0] + for module in ["cython", "filelock", "setuptools"] + ) @pytest.mark.requires_cython @pytest.mark.parametrize(['codestring', 'args', 'reference'], [ From bb78bb36ad83f7a132664fe0fb3a233b1fa9253e Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Wed, 24 Apr 2024 00:15:59 +0800 Subject: [PATCH 123/305] Add towncrier entry --- doc/changes/2401.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2401.doc diff --git a/doc/changes/2401.doc b/doc/changes/2401.doc new file mode 100644 index 0000000000..ce9323ef99 --- /dev/null +++ b/doc/changes/2401.doc @@ -0,0 +1 @@ +Correct a mistake in the doc \ No newline at end of file From b9bc56d7a3f184f61447c38ce0ae4c4081b467f9 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Apr 2024 14:07:02 -0400 Subject: [PATCH 124/305] Fix raise on too large min_step in krylov --- qutip/solver/integrator/krylov.py | 14 ++++++++------ qutip/tests/solver/test_sesolve.py | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/qutip/solver/integrator/krylov.py b/qutip/solver/integrator/krylov.py index 5559020982..7e2d9e1d79 100644 --- a/qutip/solver/integrator/krylov.py +++ b/qutip/solver/integrator/krylov.py @@ -51,7 +51,7 @@ def _prepare(self): krylov_tridiag, krylov_basis = \ self._lanczos_algorithm(rand_ket(N).data) if ( - krylov_tridiag.shape[0] < self.options["krylov_dim"] + krylov_tridiag.shape[0] < krylov_dim or krylov_tridiag.shape[0] == N ): self._max_step = np.inf @@ -138,20 +138,22 @@ def krylov_error(t): self._compute_psi(t, *reduced_state) ) / self.options["atol"]) - dt = self.options["min_step"] + # Under 0 will cause an infinite loop in the while loop bellow. + dt = max(self.options["min_step"], 1e-14) + max_step = max(self.options["max_step"], dt) err = krylov_error(dt) if err > 0: - ValueError( + raise ValueError( f"With the krylov dim of {self.options['krylov_dim']}, the " f"error with the minimum step {dt} is {err}, higher than the " f"desired tolerance of {self.options['atol']}." ) - while krylov_error(dt * 10) < 0 and dt < self.options["max_step"]: + while krylov_error(dt * 10) < 0 and dt < max_step: dt *= 10 - if dt > self.options["max_step"]: - return self.options["max_step"] + if dt > max_step: + return max_step sol = root_scalar(f=krylov_error, bracket=[dt, dt * 10], method="brentq", xtol=self.options['atol']) diff --git a/qutip/tests/solver/test_sesolve.py b/qutip/tests/solver/test_sesolve.py index cfa5ee86ea..af247c0aec 100644 --- a/qutip/tests/solver/test_sesolve.py +++ b/qutip/tests/solver/test_sesolve.py @@ -301,9 +301,19 @@ def test_krylovsolve(always_compute_step): e_op.dims = H.dims tlist = np.linspace(0, 1, 11) ref = sesolve(H, psi0, tlist, e_ops=[e_op]).expect[0] - options = {"always_compute_step", always_compute_step} - krylov_sol = krylovsolve(H, psi0, tlist, 20, e_ops=[e_op]).expect[0] - np.testing.assert_allclose(ref, krylov_sol) + options = {"always_compute_step": always_compute_step} + krylov_sol = krylovsolve(H, psi0, tlist, 20, e_ops=[e_op], options=options) + np.testing.assert_allclose(ref, krylov_sol.expect[0]) + + +def test_krylovsolve_error(): + H = qutip.rand_herm(256, density=0.2) + psi0 = qutip.basis([256], [255]) + tlist = np.linspace(0, 1, 11) + options = {"min_step": 1e10} + with pytest.raises(ValueError) as err: + krylovsolve(H, psi0, tlist, 20, options=options) + assert "error with the minimum step" in str(err.value) def test_feedback(): From 984a376bb5703c6800204a53cf857c2b0dffffba Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Apr 2024 14:57:28 -0400 Subject: [PATCH 125/305] Fix macos version in runner --- .github/workflows/tests.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f758c46f1d..0bf0b64df1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,7 +102,9 @@ jobs: # Mac # Mac has issues with MKL since september 2022. - case-name: macos - os: macos-latest + # setup-miniconda not compatible with macos-latest yet. + # https://github.com/conda-incubator/setup-miniconda/issues/344 + os: macos-13 python-version: "3.11" condaforge: 1 nomkl: 1 @@ -119,7 +121,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true python-version: ${{ matrix.python-version }} From 998fdb0daaaa258d6f1824767fccd0453f5b087b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Apr 2024 15:01:11 -0400 Subject: [PATCH 126/305] Try macos-12 --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0bf0b64df1..7a6c082d46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -102,9 +102,9 @@ jobs: # Mac # Mac has issues with MKL since september 2022. - case-name: macos - # setup-miniconda not compatible with macos-latest yet. + # setup-miniconda not compatible with macos-latest presently. # https://github.com/conda-incubator/setup-miniconda/issues/344 - os: macos-13 + os: macos-12 python-version: "3.11" condaforge: 1 nomkl: 1 From f4b39c0d11f9d3f1e0f450e169bcaf688c68f52c Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 23 Apr 2024 15:25:56 -0400 Subject: [PATCH 127/305] Add towncrier --- doc/changes/2403.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2403.doc diff --git a/doc/changes/2403.doc b/doc/changes/2403.doc new file mode 100644 index 0000000000..64c49283c6 --- /dev/null +++ b/doc/changes/2403.doc @@ -0,0 +1 @@ +Improve guide-settings page. \ No newline at end of file From e595e75cc789308d594d2a38294269e825e8686d Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 24 Apr 2024 12:22:25 -0400 Subject: [PATCH 128/305] Increase test_nm_mcsolve.test_super_H tolerance --- qutip/tests/solver/test_nm_mcsolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index e47b638e4b..7f1e30e240 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -575,7 +575,7 @@ def test_super_H(): qutip.liouvillian(H), state, times, ops_and_rates, e_ops, ntraj=ntraj, target_tol=0.1, options={'map': 'serial'}, ) - np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.5) + np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.65) def test_NonMarkovianMCSolver_run(): From f8ae77d88471fe47de4bd64b22fe85f0e803adc2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 24 Apr 2024 16:06:22 -0400 Subject: [PATCH 129/305] Faster dm norm in solvers --- qutip/solver/solver_base.py | 5 ++++- qutip/tests/solver/test_mesolve.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index d27c5f2786..3ab1990f3b 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -102,7 +102,10 @@ def _restore_state(self, data, *, copy=True): state = Qobj(data, **self._state_metadata, copy=copy) if data.shape[1] == 1 and self._options['normalize_output']: - state = state * (1 / state.norm()) + if state._isherm: + state = state * (1 / state.tr()) + else: + state = state * (1 / state.norm()) return state diff --git a/qutip/tests/solver/test_mesolve.py b/qutip/tests/solver/test_mesolve.py index 5a79d59dda..fc4677bd46 100644 --- a/qutip/tests/solver/test_mesolve.py +++ b/qutip/tests/solver/test_mesolve.py @@ -206,7 +206,7 @@ def testME_TDDecayliouvillian(self, c_ops): def test_mesolve_normalization(self, state_type): # non-hermitean H causes state to evolve non-unitarily H = qutip.Qobj([[1, -0.1j], [-0.1j, 1]]) - H = qutip.sprepost(H, H) # ensure use of MeSolve + H = qutip.spre(H) + qutip.spost(H.dag()) # ensure use of MeSolve psi0 = qutip.basis(2, 0) options = {"normalize_output": True, "progress_bar": None} From 477ed2830a3130728e7eea2365f7cc8dbd9ffbf3 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Apr 2024 09:46:19 -0400 Subject: [PATCH 130/305] Add comments on missing liouvillian check --- qutip/solver/solver_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 3ab1990f3b..ca91a1e4ae 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -85,6 +85,9 @@ def _prepare_state(self, state): self._state_metadata = { 'dims': state._dims, + # This is herm flag take for granted that the liouvillian keep + # hermiticity. But we do not check user passed super operator for + # anything other than dimensions. 'isherm': state.isherm and not (self.rhs.dims == state.dims) } if self.rhs.dims[1] == state.dims: From 9e0279b53946fd6641c9e29abea25b18219dce72 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 25 Apr 2024 10:09:08 -0400 Subject: [PATCH 131/305] Use isoper --- qutip/solver/solver_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index ca91a1e4ae..c35824bdc3 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -105,7 +105,7 @@ def _restore_state(self, data, *, copy=True): state = Qobj(data, **self._state_metadata, copy=copy) if data.shape[1] == 1 and self._options['normalize_output']: - if state._isherm: + if state.isoper: state = state * (1 / state.tr()) else: state = state * (1 / state.norm()) From 23d07922b7cb80bd1434cd03896a0471236006bd Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Fri, 26 Apr 2024 00:53:37 +0800 Subject: [PATCH 132/305] Fix #2156 code of Bloch animation in doc --- doc/guide/guide-bloch.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/doc/guide/guide-bloch.rst b/doc/guide/guide-bloch.rst index ad351a5f51..8ca7da8a8b 100644 --- a/doc/guide/guide-bloch.rst +++ b/doc/guide/guide-bloch.rst @@ -323,25 +323,19 @@ Directly Generating an Animation The code to directly generate an mp4 movie of the Qubit decay is as follows :: from matplotlib import pyplot, animation - from mpl_toolkits.mplot3d import Axes3D fig = pyplot.figure() - ax = Axes3D(fig, azim=-40, elev=30) + ax = fig.add_subplot(azim=-40, elev=30, projection="3d") sphere = qutip.Bloch(axes=ax) def animate(i): sphere.clear() - sphere.add_vectors([np.sin(theta), 0, np.cos(theta)]) + sphere.add_vectors([np.sin(theta), 0, np.cos(theta)], ["r"]) sphere.add_points([sx[:i+1], sy[:i+1], sz[:i+1]]) sphere.make_sphere() return ax - def init(): - sphere.vector_color = ['r'] - return ax - - ani = animation.FuncAnimation(fig, animate, np.arange(len(sx)), - init_func=init, blit=False, repeat=False) + ani = animation.FuncAnimation(fig, animate, np.arange(len(sx)), blit=False, repeat=False) ani.save('bloch_sphere.mp4', fps=20) The resulting movie may be viewed here: `bloch_decay.mp4 `_ From 32822eebc985703309f7cf7fd5bb2e8a227bdacf Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Fri, 26 Apr 2024 01:00:54 +0800 Subject: [PATCH 133/305] Add towncrier entry --- doc/changes/2409.doc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/changes/2409.doc diff --git a/doc/changes/2409.doc b/doc/changes/2409.doc new file mode 100644 index 0000000000..06b6309aeb --- /dev/null +++ b/doc/changes/2409.doc @@ -0,0 +1,2 @@ +Fix #2156 +Correct a sample of code in the doc \ No newline at end of file From 847723423152be34762369e84904091a16265476 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Apr 2024 12:59:50 -0400 Subject: [PATCH 134/305] Set minimum python version; Improve installation.rst Exclude numpy 2 from requirements --- doc/installation.rst | 56 ++++++++++++-------------------------------- setup.cfg | 9 +++---- 2 files changed, 20 insertions(+), 45 deletions(-) diff --git a/doc/installation.rst b/doc/installation.rst index fbed3cea04..7a91d984ff 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -36,11 +36,11 @@ The following packages are currently required: +----------------+--------------+-----------------------------------------------------+ | Package | Version | Details | +================+==============+=====================================================+ -| **Python** | 3.6+ | | +| **Python** | 3.9+ | 3.6+ for version 4.7 | +----------------+--------------+-----------------------------------------------------+ -| **NumPy** | 1.16+ | | +| **NumPy** | 1.22+ <2.0 | 1.16+ for version 4.7 | +----------------+--------------+-----------------------------------------------------+ -| **SciPy** | 1.0+ | Lower versions may have missing features. | +| **SciPy** | 1.8+ | 1.0+ for version 4.7 | +----------------+--------------+-----------------------------------------------------+ @@ -54,19 +54,21 @@ In addition, there are several optional packages that provide additional functio | ``matplotlib`` | 1.2.1+ | Needed for all visualisation tasks. | +--------------------------+--------------+-----------------------------------------------------+ | ``cython`` | 0.29.20+ | Needed for compiling some time-dependent | -| | | Hamiltonians. | +| ``setuptools`` | | Hamiltonians. Cython needs a working C++ compiler. | +| ``filelock`` | | | +--------------------------+--------------+-----------------------------------------------------+ | ``cvxpy`` | 1.0+ | Needed to calculate diamond norms. | +--------------------------+--------------+-----------------------------------------------------+ -| C++ | GCC 4.7+, | Needed for compiling Cython files, made when | -| Compiler | MS VS 2015 | using string-format time-dependence. | -+--------------------------+--------------+-----------------------------------------------------+ | ``pytest``, | 5.3+ | For running the test suite. | | ``pytest-rerunfailures`` | | | +--------------------------+--------------+-----------------------------------------------------+ | LaTeX | TeXLive 2009+| Needed if using LaTeX in matplotlib figures, or for | | | | nice circuit drawings in IPython. | +--------------------------+--------------+-----------------------------------------------------+ +| ``loky``, ``mpi4py`` | | Extra parallel map back-ends. | ++--------------------------+--------------+-----------------------------------------------------+ +| ``tqdm`` | | Extra progress bars back-end. | ++--------------------------+--------------+-----------------------------------------------------+ In addition, there are several additional packages that are not dependencies, but may give you a better programming experience. `IPython `_ provides an improved text-based Python interpreter that is far more full-featured that the default interpreter, and runs in a terminal. @@ -126,23 +128,6 @@ You activate the new environment by running You can also install any more optional packages you want with ``conda install``, for example ``matplotlib``, ``ipython`` or ``jupyter``. -Installation of the pre-release of version 5 -============================================ - -QuTiP version 5 has been in development for some time and brings many new features, heavily reworks the core functionalities of QuTiP. -It is available as a pre-release on PyPI. Anyone wanting to try the new features can install it with: - -.. code-block:: bash - - pip install --pre qutip - -We expect the pre-release to fully work. -If you find any bugs, confusing documentation or missing features, please tell create an issue on `github `_. - -This version breaks compatibility with QuTiP 4.7 in many small ways. -Please see the :doc:`changelog` for a list of changes, new features and deprecations. - - .. _install-from-source: Installing from Source @@ -182,11 +167,11 @@ Direct Setuptools Source Builds This is the method to have the greatest amount of control over the installation, but it the most error-prone and not recommended unless you know what you are doing. You first need to have all the runtime dependencies installed. The most up-to-date requirements will be listed in ``pyproject.toml`` file, in the ``build-system.requires`` key. -As of the 4.6.0 release, the build requirements can be installed with +As of the 5.0.0 release, the build requirements can be installed with .. code-block:: bash - pip install setuptools wheel packaging 'cython>=0.29.20' 'numpy>=1.16.6,<1.20' 'scipy>=1.0' + pip install setuptools wheel packaging cython 'numpy<2.0.0' scipy or similar with ``conda`` if you prefer. You will also need to have a functional C++ compiler installed on your system. @@ -196,17 +181,7 @@ To install QuTiP from the source code run: .. code-block:: bash - python setup.py install - -To install OpenMP support, if available, run: - -.. code-block:: bash - - python setup.py install --with-openmp - -This will attempt to load up OpenMP libraries during the compilation process, which depends on you having suitable C++ compiler and library support. -If you are on Linux this is probably already done, but the compiler macOS ships with does not have OpenMP support. -You will likely need to refer to external operating-system-specific guides for more detail here, as it may be very non-trivial to correctly configure. + pip install . If you wish to contribute to the QuTiP project, then you will want to create your own fork of `the QuTiP git repository `_, clone this to a local folder, and install it into your Python environment using: @@ -252,12 +227,11 @@ Verifying the Installation QuTiP includes a collection of built-in test scripts to verify that an installation was successful. To run the suite of tests scripts you must also have the ``pytest`` testing library. -After installing QuTiP, leave the installation directory, run Python (or IPython), and call: +After installing QuTiP, leave the installation directory and call: -.. code-block:: python +.. code-block:: bash - import qutip.testing - qutip.testing.run() + pytest qutip/qutip/tests This will take between 10 and 30 minutes, depending on your computer. At the end, the testing report should report a success; it is normal for some tests to be skipped, and for some to be marked "xfail" in yellow. diff --git a/setup.cfg b/setup.cfg index 1ea0a3bc71..ada7b012cc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,15 +30,16 @@ platforms = Linux, Mac OSX, Unix, Windows packages = find: include_package_data = True zip_safe = False +python_requires = >=3.9 install_requires = - numpy>=1.22 + numpy>=1.22,<2.0.0 scipy>=1.8 packaging setup_requires = - numpy>=1.19 + numpy>=1.19,<2.0.0 scipy>=1.8 cython>=0.29.20; python_version>='3.10' - cython>=0.29.20,<3.0.3; python_version<='3.9' + cython>=0.29.20,<3.0.0; python_version<='3.9' packaging [options.packages.find] @@ -48,7 +49,7 @@ include = qutip* graphics = matplotlib>=1.2.1 runtime_compilation = cython>=0.29.20; python_version>='3.10' - cython>=0.29.20,<3.0.3; python_version<='3.9' + cython>=0.29.20,<3.0.0; python_version<='3.9' filelock setuptools semidefinite = From 3543a42142cbea81cfa93e31511528457cc4a389 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Apr 2024 13:29:07 -0400 Subject: [PATCH 135/305] Increase doc build python version --- .github/workflows/build_documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index b5c6eb9cf6..66550070c0 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.8' + python-version: '3.11' - name: Install documentation dependencies run: | From fb9a8ff894a3ebab320223ba62e9b2df01e35141 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Apr 2024 13:34:03 -0400 Subject: [PATCH 136/305] Add towncrier --- doc/changes/2413.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2413.misc diff --git a/doc/changes/2413.misc b/doc/changes/2413.misc new file mode 100644 index 0000000000..9c0c341c05 --- /dev/null +++ b/doc/changes/2413.misc @@ -0,0 +1 @@ +Implicitly set minimum python version to 3.9 From dc4c19128e4486ff8433dbd06f1c3ed74c4fa5ab Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Apr 2024 13:55:29 -0400 Subject: [PATCH 137/305] Increase cython version used to build docs --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index ac2761b160..5ad29757e4 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,7 +4,7 @@ backcall==0.2.0 certifi==2023.7.22 chardet==4.0.0 cycler==0.10.0 -Cython==0.29.33 +Cython==3.0.8 decorator==5.1.1 docutils==0.18.1 idna==3.7 From 8f3df5c4ab6c9ff90d37815f2785bef5fa4d73c0 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Apr 2024 14:25:31 -0400 Subject: [PATCH 138/305] Increase scipy version used to build docs --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index 5ad29757e4..f8f361dba6 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -29,7 +29,7 @@ pyparsing==3.0.9 python-dateutil==2.8.2 pytz==2023.3 requests==2.31.0 -scipy==1.10.1 +scipy==1.11.4 six==1.16.0 snowballstemmer==2.2.0 Sphinx==6.1.3 From 49b6db1f2c2add4254960df18cc67ea4b2d59827 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 30 Apr 2024 15:43:14 -0400 Subject: [PATCH 139/305] fix editable install? --- .github/workflows/build_documentation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index 66550070c0..d432b5135d 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -27,7 +27,7 @@ jobs: run: | # Build without build isolation so that we use the build # dependencies already installed from doc/requirements.txt. - python -m pip install -e .[full] --no-build-isolation + python -m pip install -e .[full] --no-build-isolation --config-settings editable_mode=compat # Install in editable mode so it doesn't matter if we import from # inside the installation directory, otherwise we can get some errors # because we're importing from the wrong location. From 44f50d6b329a04fe39cd4f19204ca489542e5a94 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 1 May 2024 13:25:09 -0400 Subject: [PATCH 140/305] Add towncrier --- doc/changes/2327.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2327.feature diff --git a/doc/changes/2327.feature b/doc/changes/2327.feature new file mode 100644 index 0000000000..de21691907 --- /dev/null +++ b/doc/changes/2327.feature @@ -0,0 +1 @@ +Add types hints in core solvers functions. From 5605b90feeaeab3a3b343a2fa88013163a9b931b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 1 May 2024 13:28:45 -0400 Subject: [PATCH 141/305] Pep8 --- qutip/core/properties.py | 2 +- qutip/core/qobj.py | 2 +- qutip/solver/mesolve.py | 2 +- qutip/solver/multitraj.py | 2 +- qutip/solver/result.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qutip/core/properties.py b/qutip/core/properties.py index 47b29cc968..5bfa417bb0 100644 --- a/qutip/core/properties.py +++ b/qutip/core/properties.py @@ -32,4 +32,4 @@ def issuper(x: Qobj | QobjEvo): def isherm(x: Qobj): if not isinstance(x, Qobj): raise TypeError(f"Invalid input type, got {type(x)}, exected Qobj") - return x.isherm \ No newline at end of file + return x.isherm diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 5f74750e27..b3d9937479 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -268,7 +268,7 @@ def _initialize_data(self, arg, dims, copy): def __init__( self, - arg: ArrayLike | Any =None, + arg: ArrayLike | Any = None, dims: list[list[int]] | list[list[list[int]]] | Dimensions = None, copy: bool = True, superrep: str = None, diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index dab1084e82..c3162872a1 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -209,7 +209,7 @@ def __init__( H: Qobj | QobjEvo, c_ops: Qobj | QobjEvo | list[Qobj | QobjEvo] = None, *, - options: dict=None + options: dict = None, ): _time_start = time() diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 0c91f5ba04..e31285982d 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -81,7 +81,7 @@ def __init__(self, rhs, *, options=None): self._state_metadata = {} self.stats = self._initialize_stats() - def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence=None): + def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence = None): """ Set the initial state and time for a step evolution. diff --git a/qutip/solver/result.py b/qutip/solver/result.py index beb3d5fb3b..ecc4881b64 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -221,7 +221,7 @@ class Result(_BaseResult): times: list[float] states: list[Qobj] options: ResultOptions - e_data : dict[Any, list[Any]] + e_data: dict[Any, list[Any]] def __init__( self, From 059417bf3e19d6f6db44a719d84d7e9d7763c200 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 1 May 2024 13:55:12 -0400 Subject: [PATCH 142/305] import used types --- qutip/solver/krylovsolve.py | 2 ++ qutip/solver/mcsolve.py | 6 ++++-- qutip/solver/multitraj.py | 9 +++++++-- qutip/solver/propagator.py | 2 ++ qutip/solver/result.py | 4 ++-- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/qutip/solver/krylovsolve.py b/qutip/solver/krylovsolve.py index 6e3b51d143..b03c4c86bd 100644 --- a/qutip/solver/krylovsolve.py +++ b/qutip/solver/krylovsolve.py @@ -2,7 +2,9 @@ from .. import QobjEvo, Qobj from .sesolve import SESolver +from .result import Result from numpy.typing import ArrayLike +from typing import Any, Callable def krylovsolve( diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 09fe705127..03183f9fda 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -4,6 +4,7 @@ from numpy.typing import ArrayLike from numpy.random import SeedSequence from ..core import QobjEvo, spre, spost, Qobj, unstack_columns +from ..typing import QobjEvoLike from .multitraj import MultiTrajSolver, _MultiTrajRHS from .solver_base import Solver, Integrator, _solver_deprecation from .result import McResult, McTrajectoryResult, McResultImprovedSampling @@ -11,6 +12,7 @@ from ._feedback import _QobjFeedback, _DataFeedback, _CollapseFeedback import qutip.core.data as _data from time import time +from typing import Any, Callable def mcsolve( @@ -27,7 +29,7 @@ def mcsolve( target_tol: float = None, timeout: float = None, **kwargs, -) -> Result: +) -> McResult: r""" Monte Carlo evolution of a state vector :math:`|\psi \rangle` for a given Hamiltonian and sets of collapse operators. Options for the @@ -514,7 +516,7 @@ def run( target_tol: float = None, timeout: float = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, - ) -> Result: + ) -> McResult: # Overridden to sample the no-jump trajectory first. Then, the no-jump # probability is used as a lower-bound for random numbers in future # monte carlo runs diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index e31285982d..295e6a5d88 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -2,8 +2,13 @@ from .parallel import _get_map from time import time from .solver_base import Solver -from ..core import QobjEvo +from ..core import QobjEvo, Qobj import numpy as np +from numpy.typing import ArrayLike +from numpy.random import SeedSequence +from numbers import Number +from typing import Any, Callable + __all__ = ["MultiTrajSolver"] @@ -107,7 +112,7 @@ def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence = None): self._integrator.set_state(t0, self._prepare_state(state), generator) def step( - self, t | Number, *, args: dict[str, Any] = None, copy: bool = True + self, t: Number, *, args: dict[str, Any] = None, copy: bool = True ) -> Qobj: """ Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index 3770307bee..33d5956ab7 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -11,6 +11,8 @@ from .heom.bofin_solvers import HEOMSolver from .solver_base import Solver from .multitraj import MultiTrajSolver +from numbers import Number +from typing import Any def propagator( diff --git a/qutip/solver/result.py b/qutip/solver/result.py index ecc4881b64..3ee057a8d4 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1,6 +1,6 @@ """ Class for solve function results""" -from typing import TypedDict, Any +from typing import TypedDict, Any, Callable import numpy as np from numpy.typing import ArrayLike from numbers import Number @@ -225,7 +225,7 @@ class Result(_BaseResult): def __init__( self, - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]], options: ResultOptions, *, solver: str = None, From 25b3060aa32fd29222bd62c40745d159bdc2b8b7 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 1 May 2024 14:37:26 -0400 Subject: [PATCH 143/305] Remove 3.9 support --- .github/workflows/build.yml | 16 ++++++++-------- .github/workflows/tests.yml | 13 ------------- qutip/solver/multitraj.py | 2 +- qutip/solver/propagator.py | 1 + setup.cfg | 2 +- 5 files changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c08e003ea1..b3e15b5334 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,7 +57,7 @@ jobs: # For the sdist we should be as conservative as possible with our # Python version. This should be the lowest supported version. This # means that no unsupported syntax can sneak through. - python-version: '3.9' + python-version: '3.10' - name: Install pip build run: | @@ -107,8 +107,8 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-latest] env: - # Set up wheels matrix. This is CPython 3.9--3.12 for all OS targets. - CIBW_BUILD: "cp3{9,10,11,12}-*" + # Set up wheels matrix. This is CPython 3.10--3.12 for all OS targets. + CIBW_BUILD: "cp3{10,11,12}-*" # Numpy and SciPy do not supply wheels for i686 or win32 for # Python 3.10+, so we skip those: CIBW_SKIP: "*-musllinux* cp3{10,11,12}-manylinux_i686 cp3{10,11,12}-win32" @@ -121,7 +121,7 @@ jobs: name: Install Python with: # This is about the build environment, not the released wheel version. - python-version: '3.9' + python-version: '3.10' - name: Install cibuildwheel run: | @@ -165,12 +165,12 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.9' + python-version: '3.10' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp39-cp39-manylinux*.whl + python -m pip install wheels/*-cp310-cp310-manylinux*.whl python -c 'import qutip; print(qutip.__version__); assert "dev" not in qutip.__version__; assert "+" not in qutip.__version__' # We built the zipfile for convenience distributing to Windows users on @@ -198,12 +198,12 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.9' + python-version: '3.10' - name: Verify this is not a dev version shell: bash run: | - python -m pip install wheels/*-cp39-cp39-manylinux*.whl + python -m pip install wheels/*-cp310-cp310-manylinux*.whl python -c 'import qutip; print(qutip.__version__); assert "dev" not in qutip.__version__; assert "+" not in qutip.__version__' # We built the zipfile for convenience distributing to Windows users on diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a6c082d46..12c067acf5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,19 +37,6 @@ jobs: # the lack of a variable is _always_ false-y, and the defaults lack all # the special cases. include: - # Python 3.9, Scipy 1.7, numpy 1.22 - # On more version than suggested by SPEC 0 - # https://scientific-python.org/specs/spec-0000/ - # There are deprecation warnings when using cython 0.29.X - - case-name: Old setup - os: ubuntu-latest - python-version: "3.9" - scipy-requirement: ">=1.8,<1.9" - numpy-requirement: ">=1.22,<1.23" - condaforge: 1 - oldcython: 1 - pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" - # Python 3.10, no mkl, scipy 1.9, numpy 1.23 # Scipy 1.9 did not support cython 3.0 yet. # cython#17234 diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 295e6a5d88..b48c5fdc73 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -109,7 +109,7 @@ def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence = None): """ seeds = self._read_seed(seed, 1) generator = self._get_generator(seeds[0]) - self._integrator.set_state(t0, self._prepare_state(state), generator) + self._integrator.set_state(t0, self._prepare_state(state0), generator) def step( self, t: Number, *, args: dict[str, Any] = None, copy: bool = True diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index 33d5956ab7..2a646b3701 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -190,6 +190,7 @@ def __init__( self.solver = system else: Hevo = QobjEvo(system, args=args) + c_ops = c_ops if c_ops is not None else [] c_ops = [QobjEvo(op, args=args) for op in c_ops] if Hevo.issuper or c_ops: self.solver = MESolver(Hevo, c_ops=c_ops, options=options) diff --git a/setup.cfg b/setup.cfg index ada7b012cc..c54f59c7b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,7 @@ platforms = Linux, Mac OSX, Unix, Windows packages = find: include_package_data = True zip_safe = False -python_requires = >=3.9 +python_requires = >=3.10 install_requires = numpy>=1.22,<2.0.0 scipy>=1.8 From b6f0035ef95561ef9bbb6a14afdd441b8d5f4eb4 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 1 May 2024 15:07:02 -0400 Subject: [PATCH 144/305] Update apidoc --- doc/apidoc/functions.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/apidoc/functions.rst b/doc/apidoc/functions.rst index f64707a415..f5d58db96a 100644 --- a/doc/apidoc/functions.rst +++ b/doc/apidoc/functions.rst @@ -34,7 +34,10 @@ Quantum Objects --------------- .. automodule:: qutip.core.qobj - :members: ptrace, issuper, isoper, isoperket, isoperbra, isket, isbra, isherm + :members: ptrace + +.. automodule:: qutip.core.properties + :members: issuper, isoper, isoperket, isoperbra, isket, isbra, isherm Random Operators and States From c09b4eb0de217cafaf876ee23ee52f9b5cb7490d Mon Sep 17 00:00:00 2001 From: owenagnel Date: Sun, 5 May 2024 17:30:14 +0100 Subject: [PATCH 145/305] Improved unitary case speedup --- qutip/core/metrics.py | 61 +++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index 0df7b19f36..cc61adb9e9 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -440,6 +440,9 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, The diamond norm SDP is solved by using `CVXPY `_. + If B is provided and both A and B are unitaries, a special optimised + case of the diamond norm is used. + Parameters ---------- A : Qobj @@ -477,24 +480,16 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, # for instance, by both pyGSTi and SchattenNorms.jl. (By contrast, # QETLAB uses the dual problem.) - # Check if A and B are both unitaries. If so, then we can without - # loss of generality choose B to be the identity by using the - # unitary invariance of the diamond norm, - # || A - B ||_♢ = || A B⁺ - I ||_♢. - # Then, using the technique mentioned by each of Johnston and - # da Silva, - # || A B⁺ - I ||_♢ = max_{i, j} | \lambda_i(A B⁺) - \lambda_j(A B⁺) |, - # where \lambda_i(U) is the ith eigenvalue of U. - - # There's a lot of conditions to check for this path. Only check if they - # aren't superoperators. The difference of unitaries optimization is - # currently only implemented for d == 2. Much of the code below is more - # general, though, in anticipation of generalizing the optimization. + # Check if A and B are both unitaries. If so we can use a trick + # discussed in D. Aharonov, A. Kitaev, and N. Nisan. (1998). + # In general we can find the eigenvalues of AB⁺ and the distance + # d between the origin and compelx hull of these. Plugging + # this into 2√1-d² gives the diamond norm. + if ( not force_solve and B is not None and A.isoper and B.isoper - and A.shape[0] == 2 ): # Make an identity the same size as A and B to # compare against. @@ -507,17 +502,11 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, (A * A.dag() - I).norm() < 1e-6 ): # Now we are on the fast path, so let's compute the - # eigenvalues, then find the diameter of the smallest circle - # containing all of them. - # - # For now, this is only implemented for dim = 2, such that - # generalizing here will allow for generalizing the optimization. - # A reasonable approach would probably be to use Welzl's algorithm - # (https://en.wikipedia.org/wiki/Smallest-circle_problem). + # eigenvalues, then find the distance between origin and hull U = A * B.dag() eigs = U.eigenenergies() - eig_distances = np.abs(eigs[:, None] - eigs[None, :]) - return np.max(eig_distances) + d = _find_poly_distance(eigs) + return 2 * np.sqrt(1 - d**2) # plug into formula # Force the input superoperator to be a Choi matrix. J = to_choi(A) @@ -583,3 +572,29 @@ def unitarity(oper): """ Eu = _to_superpauli(oper).full()[1:, 1:] return np.linalg.norm(Eu, 'fro')**2 / len(Eu) + + +def _find_poly_distance(eigenvals: np.ndarray) -> float: + """Function to find the distance between origin and the + convex hull of eigenvalues.""" + phases = np.angle(eigenvals) + pos_max = phases.max() + neg_min = phases.min() + pos_min = np.where(phases > 0, phases, np.inf).min() + neg_max = np.where(phases <= 0, phases, -np.inf).max() + + if neg_min > 0: # all eigenvals have positive phase, hull is above x axis + return np.cos((pos_max - pos_min) / 2) + + if pos_max <= 0: # all eigenvals have negative phase, hull is below x axis + return np.cos((np.abs(neg_min) - np.abs(neg_max)) / 2) + + big_angle = pos_max - neg_min + small_angle = pos_min - neg_max + if big_angle >= np.pi: + if small_angle <= np.pi: # hull contains the origin + return 0 + else: # hull is left of y axis + return np.cos((2 * np.pi - small_angle) / 2) + else: # hull is right of y axis + return np.cos(big_angle / 2) From 4814059afb2f2c44969d5cf9320ff0702b14f5ba Mon Sep 17 00:00:00 2001 From: owenagnel Date: Sun, 5 May 2024 19:19:49 +0100 Subject: [PATCH 146/305] changelog --- doc/changes/2416.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/changes/2416.feature diff --git a/doc/changes/2416.feature b/doc/changes/2416.feature new file mode 100644 index 0000000000..4e553dd048 --- /dev/null +++ b/doc/changes/2416.feature @@ -0,0 +1,2 @@ +Updated `qutip.core.metrics.dnorm` to have an efficient speedup when findind the difference of two unitaries. We use a result on page 19 of +D. Aharonov, A. Kitaev, and N. Nisan, (1998). \ No newline at end of file From 8e5cbb6cbdc03f77b080593143e26bc16c7137e0 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Sun, 5 May 2024 20:20:28 +0100 Subject: [PATCH 147/305] Doc improvements --- doc/biblio.rst | 5 +++++ doc/changes/2416.feature | 2 +- qutip/core/metrics.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/doc/biblio.rst b/doc/biblio.rst index 1bdabce891..486bf2a963 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -40,6 +40,11 @@ Bibliography C. Wood, J. Biamonte, D. G. Cory, *Tensor networks and graphical calculus for open quantum systems*. :arxiv:`1111.6950` +.. [AKN98] + D. Aharonov, A. Kitaev, and N. Nisan, *Quantum circuits with mixed states*, + in Proceedings of the thirtieth annual ACM symposium on Theory of computing, + 20–30 (1998). :arxiv:`quant-ph/9806029` + .. [dAless08] D. d’Alessandro, *Introduction to Quantum Control and Dynamics*, (Chapman & Hall/CRC, 2008). diff --git a/doc/changes/2416.feature b/doc/changes/2416.feature index 4e553dd048..e614e4e5ad 100644 --- a/doc/changes/2416.feature +++ b/doc/changes/2416.feature @@ -1,2 +1,2 @@ -Updated `qutip.core.metrics.dnorm` to have an efficient speedup when findind the difference of two unitaries. We use a result on page 19 of +Updated `qutip.core.metrics.dnorm` to have an efficient speedup when finding the difference of two unitaries. We use a result on page 19 of D. Aharonov, A. Kitaev, and N. Nisan, (1998). \ No newline at end of file diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index cc61adb9e9..f9cb0003bf 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -441,7 +441,7 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, The diamond norm SDP is solved by using `CVXPY `_. If B is provided and both A and B are unitaries, a special optimised - case of the diamond norm is used. + case of the diamond norm is used. See [AKN98]_. Parameters ---------- From 18a75b979633eaacfb7cb70474a1bdc4d50bbc90 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 6 May 2024 14:14:56 +0900 Subject: [PATCH 148/305] Created multitrajresult module --- doc/apidoc/classes.rst | 6 +- qutip/solver/__init__.py | 1 + qutip/solver/mcsolve.py | 2 +- qutip/solver/multitraj.py | 3 +- qutip/solver/multitrajresult.py | 1120 ++++++++++++++++++++++++++++ qutip/solver/nm_mcsolve.py | 2 +- qutip/solver/result.py | 1110 +-------------------------- qutip/solver/stochastic.py | 3 +- qutip/tests/solver/test_mcsolve.py | 1 - qutip/tests/solver/test_results.py | 5 +- 10 files changed, 1135 insertions(+), 1118 deletions(-) create mode 100644 qutip/solver/multitrajresult.py diff --git a/doc/apidoc/classes.rst b/doc/apidoc/classes.rst index 3404616a22..a4dac6cf20 100644 --- a/doc/apidoc/classes.rst +++ b/doc/apidoc/classes.rst @@ -231,7 +231,7 @@ Solver Options and Results :inherited-members: :exclude-members: add_processor, add -.. autoclass:: qutip.solver.result.MultiTrajResult +.. autoclass:: qutip.solver.multitrajresult.MultiTrajResult :members: :inherited-members: :exclude-members: add_processor, add, add_end_condition @@ -240,11 +240,11 @@ Solver Options and Results :show-inheritance: :members: -.. autoclass:: qutip.solver.result.McResult +.. autoclass:: qutip.solver.multitrajresult.McResult :show-inheritance: :members: -.. autoclass:: qutip.solver.result.NmmcResult +.. autoclass:: qutip.solver.multitrajresult.NmmcResult :show-inheritance: :members: diff --git a/qutip/solver/__init__.py b/qutip/solver/__init__.py index 6714cae70b..9725b3855c 100644 --- a/qutip/solver/__init__.py +++ b/qutip/solver/__init__.py @@ -1,4 +1,5 @@ from .result import * +from .multitrajresult import * from .options import * import qutip.solver.integrator as integrator from .integrator import IntegratorException diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 1e0e94e230..aa283573bd 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -4,7 +4,7 @@ from ..core import QobjEvo, spre, spost, Qobj, unstack_columns from .multitraj import MultiTrajSolver, _MultiTrajRHS from .solver_base import Solver, Integrator, _solver_deprecation -from .result import McResult +from .multitrajresult import McResult from .mesolve import mesolve, MESolver from ._feedback import _QobjFeedback, _DataFeedback, _CollapseFeedback import qutip.core.data as _data diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index d4c8578bc7..6bd54bd7c7 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -1,4 +1,5 @@ -from .result import TrajectoryResult, MultiTrajResult +from .result import TrajectoryResult +from .multitrajresult import MultiTrajResult from .parallel import _get_map from time import time from .solver_base import Solver diff --git a/qutip/solver/multitrajresult.py b/qutip/solver/multitrajresult.py new file mode 100644 index 0000000000..765a8a1f34 --- /dev/null +++ b/qutip/solver/multitrajresult.py @@ -0,0 +1,1120 @@ +""" +This module provides result classes for multi-trajectory solvers. +Note that single trajectories are described by regular `Result` objects from the +`qutip.solver.result` module. +""" + +from typing import TypedDict +import numpy as np + +from copy import copy + +from .result import _BaseResult, TrajectoryResult +from ..core import qzero_like + +__all__ = [ + "MultiTrajResult", + "McResult", + "NmmcResult", +] + + +class MultiTrajResultOptions(TypedDict): + store_states: bool + store_final_state: bool + keep_runs_results: bool + + +class MultiTrajResult(_BaseResult): + """ + Base class for storing results for solver using multiple trajectories. + + Parameters + ---------- + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these + The ``e_ops`` parameter defines the set of values to record at + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that + operator given the state at ``t``. If the element is a function, ``f``, + the value recorded is ``f(t, state)``. + + The values are recorded in the ``.expect`` attribute of this result + object. ``.expect`` is a list, where each item contains the values + of the corresponding ``e_op``. + + Function ``e_ops`` must return a number so the average can be computed. + + options : dict + The options for this result class. + + solver : str or None + The name of the solver generating these results. + + stats : dict or None + The stats generated by the solver while producing these results. Note + that the solver may update the stats directly while producing results. + + kw : dict + Additional parameters specific to a result sub-class. + + Attributes + ---------- + times : list + A list of the times at which the expectation values and states were + recorded. + + average_states : list of :obj:`.Qobj` + The state at each time ``t`` (if the recording of the state was + requested) averaged over all trajectories as a density matrix. + + runs_states : list of list of :obj:`.Qobj` + The state for each trajectory and each time ``t`` (if the recording of + the states and trajectories was requested) + + final_state : :obj:`.Qobj`: + The final state (if the recording of the final state was requested) + averaged over all trajectories as a density matrix. + + runs_final_state : list of :obj:`.Qobj` + The final state for each trajectory (if the recording of the final + state and trajectories was requested). + + average_expect : list of array of expectation values + A list containing the values of each ``e_op`` averaged over each + trajectories. The list is in the same order in which the ``e_ops`` were + supplied and empty if no ``e_ops`` were given. + + Each element is itself an array and contains the values of the + corresponding ``e_op``, with one value for each time in ``.times``. + + std_expect : list of array of expectation values + A list containing the standard derivation of each ``e_op`` over each + trajectories. The list is in the same order in which the ``e_ops`` were + supplied and empty if no ``e_ops`` were given. + + Each element is itself an array and contains the values of the + corresponding ``e_op``, with one value for each time in ``.times``. + + runs_expect : list of array of expectation values + A list containing the values of each ``e_op`` for each trajectories. + The list is in the same order in which the ``e_ops`` were + supplied and empty if no ``e_ops`` were given. Only available if the + storing of trajectories was requested. + + The order of the elements is ``runs_expect[e_ops][trajectory][time]``. + + Each element is itself an array and contains the values of the + corresponding ``e_op``, with one value for each time in ``.times``. + + average_e_data : dict + A dictionary containing the values of each ``e_op`` averaged over each + trajectories. If the ``e_ops`` were supplied as a dictionary, the keys + are the same as in that dictionary. Otherwise the keys are the index of + the ``e_op`` in the ``.expect`` list. + + The lists of expectation values returned are the *same* lists as + those returned by ``.expect``. + + average_e_data : dict + A dictionary containing the standard derivation of each ``e_op`` over + each trajectories. If the ``e_ops`` were supplied as a dictionary, the + keys are the same as in that dictionary. Otherwise the keys are the + index of the ``e_op`` in the ``.expect`` list. + + The lists of expectation values returned are the *same* lists as + those returned by ``.expect``. + + runs_e_data : dict + A dictionary containing the values of each ``e_op`` for each + trajectories. If the ``e_ops`` were supplied as a dictionary, the keys + are the same as in that dictionary. Otherwise the keys are the index of + the ``e_op`` in the ``.expect`` list. Only available if the storing + of trajectories was requested. + + The order of the elements is ``runs_expect[e_ops][trajectory][time]``. + + The lists of expectation values returned are the *same* lists as + those returned by ``.expect``. + + runs_weights : list + For each trajectory, the weight with which that trajectory enters + averages. + + solver : str or None + The name of the solver generating these results. + + stats : dict or None + The stats generated by the solver while producing these results. + + options : :obj:`~SolverResultsOptions` + The options for this result class. + """ + + options: MultiTrajResultOptions + + def __init__( + self, e_ops, options: MultiTrajResultOptions, *, + solver=None, stats=None, **kw, + ): + super().__init__(options, solver=solver, stats=stats) + self._raw_ops = self._e_ops_to_dict(e_ops) + + self.trajectories = [] + self.num_trajectories = 0 + self.seeds = [] + + self.average_e_data = {} + self.std_e_data = {} + if self.options["keep_runs_results"]: + self.runs_e_data = {k: [] for k in self._raw_ops} + else: + self.runs_e_data = {} + + # Will be initialized at the first trajectory + self.times = None + self.e_ops = None + + # We separate all sums into terms of trajectories with specified + # absolute weight (_abs) or without (_rel). They will be initialized + # when the first trajectory of the respective type is added. + self._sum_rel = None + self._sum_abs = None + # Number of trajectories without specified absolute weight + self._num_rel_trajectories = 0 + # Needed for merging results + self._weight_info = [] + # Needed for target tolerance computation + self._total_abs_weight = np.array(0) + + self._post_init(**kw) + + @property + def _store_average_density_matrices(self) -> bool: + return ( + self.options["store_states"] + or (self.options["store_states"] is None and self._raw_ops == {}) + ) and not self.options["keep_runs_results"] + + @property + def _store_final_density_matrix(self) -> bool: + return ( + self.options["store_final_state"] + and not self._store_average_density_matrices + and not self.options["keep_runs_results"] + ) + + def _add_first_traj(self, trajectory): + """ + Read the first trajectory, intitializing needed data. + """ + self.times = trajectory.times + self.e_ops = trajectory.e_ops + + def _store_trajectory(self, trajectory): + self.trajectories.append(trajectory) + + def _store_weight_info(self, trajectory): + if trajectory.has_absolute_weight: + self._total_abs_weight = ( + self._total_abs_weight + trajectory.total_weight) + if len(self.trajectories) == 0: + # store weight info only if trajectories are not stored + self._weight_info.append( + (trajectory.total_weight, trajectory.has_absolute_weight)) + + def _reduce_states(self, trajectory): + if trajectory.has_absolute_weight: + self._sum_abs.reduce_states(trajectory) + else: + self._sum_rel.reduce_states(trajectory) + + def _reduce_final_state(self, trajectory): + if trajectory.has_absolute_weight: + self._sum_abs.reduce_final_state(trajectory) + else: + self._sum_rel.reduce_final_state(trajectory) + + def _reduce_expect(self, trajectory): + """ + Compute the average of the expectation values and store it in it's + multiple formats. + """ + if trajectory.has_absolute_weight: + self._sum_abs.reduce_expect(trajectory) + else: + self._sum_rel.reduce_expect(trajectory) + + self._create_e_data() + + if self.runs_e_data: + for k in self._raw_ops: + self.runs_e_data[k].append(trajectory.e_data[k]) + + def _create_e_data(self): + for i, k in enumerate(self._raw_ops): + avg = 0 + avg2 = 0 + if self._sum_abs: + avg += self._sum_abs.sum_expect[i] + avg2 += self._sum_abs.sum2_expect[i] + if self._sum_rel: + avg += ( + self._sum_rel.sum_expect[i] / self._num_rel_trajectories + ) + avg2 += ( + self._sum_rel.sum2_expect[i] / self._num_rel_trajectories + ) + + self.average_e_data[k] = list(avg) + # mean(expect**2) - mean(expect)**2 can something be very small + # negative (-1e-15) which raise an error for float sqrt. + self.std_e_data[k] = list(np.sqrt(np.abs(avg2 - np.abs(avg**2)))) + + def _increment_traj(self, trajectory): + if self.num_trajectories == 0: + self._add_first_traj(trajectory) + + if trajectory.has_absolute_weight: + if self._sum_abs is None: + self._sum_abs = _TrajectorySum( + trajectory, + self._store_average_density_matrices, + self._store_final_density_matrix) + else: + self._num_rel_trajectories += 1 + if self._sum_rel is None: + self._sum_rel = _TrajectorySum( + trajectory, + self._store_average_density_matrices, + self._store_final_density_matrix) + + self.num_trajectories += 1 + + def _no_end(self): + """ + Remaining number of trajectories needed to finish cannot be determined + by this object. + """ + return np.inf + + def _fixed_end(self): + """ + Finish at a known number of trajectories. + """ + ntraj_left = self._target_ntraj - self.num_trajectories + if ntraj_left == 0: + self.stats["end_condition"] = "ntraj reached" + return ntraj_left + + def _average_computer(self): + avg = np.array(self._sum_rel.sum_expect) / self._num_rel_trajectories + avg2 = np.array(self._sum_rel.sum2_expect) / self._num_rel_trajectories + return avg, avg2 + + def _target_tolerance_end(self): + """ + Compute the error on the expectation values using jackknife resampling. + Return the approximate number of trajectories needed to have this + error within the tolerance fot all e_ops and times. + """ + if self.num_trajectories >= self._target_ntraj: + # First make sure that "ntraj" setting is always respected + self.stats["end_condition"] = "ntraj reached" + return 0 + + if self._num_rel_trajectories <= 1: + return np.inf + avg, avg2 = self._average_computer() + target = np.array( + [ + atol + rtol * mean + for mean, (atol, rtol) in zip(avg, self._target_tols) + ] + ) + + one = np.array(1) + if self._num_rel_trajectories < self.num_trajectories: + # We only include traj. without abs. weights in this calculation. + # Since there are traj. with abs. weights., the weights don't add + # up to one. We have to consider that as follows: + # <(x - )^2> / <1> = / <1> - ^2 / <1>^2 + # and "<1>" is one minus the sum of all absolute weights + one = one - self._total_abs_weight + + target_ntraj = np.max((avg2 / one - (abs(avg) ** 2) / (one ** 2)) / + target**2 + 1) + + self._estimated_ntraj = min(target_ntraj - self._num_rel_trajectories, + self._target_ntraj - self.num_trajectories) + if self._estimated_ntraj <= 0: + self.stats["end_condition"] = "target tolerance reached" + return self._estimated_ntraj + + def _post_init(self): + self._target_ntraj = None + self._target_tols = None + self._early_finish_check = self._no_end + + self.add_processor(self._increment_traj) + store_trajectory = self.options["keep_runs_results"] + if store_trajectory: + self.add_processor(self._store_trajectory) + if self._store_average_density_matrices: + self.add_processor(self._reduce_states) + if self._store_final_density_matrix: + self.add_processor(self._reduce_final_state) + if self._raw_ops: + self.add_processor(self._reduce_expect) + self.add_processor(self._store_weight_info) + + self.stats["end_condition"] = "unknown" + + def add(self, trajectory_info): + """ + Add a trajectory to the evolution. + + Trajectories can be saved or average canbe extracted depending on the + options ``keep_runs_results``. + + Parameters + ---------- + trajectory_info : tuple of seed and trajectory + - seed: int, SeedSequence + Seed used to generate the trajectory. + - trajectory : :class:`Result` + Run result for one evolution over the times. + + Returns + ------- + remaing_traj : number + Return the number of trajectories still needed to reach the target + tolerance. If no tolerance is provided, return infinity. + """ + seed, trajectory = trajectory_info + self.seeds.append(seed) + + if not isinstance(trajectory, TrajectoryResult): + trajectory.has_weight = False + trajectory.has_absolute_weight = False + trajectory.has_time_dependent_weight = False + trajectory.total_weight = 1 + + for op in self._state_processors: + op(trajectory) + + return self._early_finish_check() + + def add_end_condition(self, ntraj, target_tol=None): + """ + Set the condition to stop the computing trajectories when the certain + condition are fullfilled. + Supported end condition for multi trajectories computation are: + + - Reaching a number of trajectories. + - Error bar on the expectation values reach smaller than a given + tolerance. + + Parameters + ---------- + ntraj : int + Number of trajectories expected. + + target_tol : float, array_like, [optional] + Target tolerance of the evolution. The evolution will compute + trajectories until the error on the expectation values is lower + than this tolerance. The error is computed using jackknife + resampling. ``target_tol`` can be an absolute tolerance, a pair of + absolute and relative tolerance, in that order. Lastly, it can be a + list of pairs of (atol, rtol) for each e_ops. + + Error estimation is done with jackknife resampling. + """ + self._target_ntraj = ntraj + self.stats["end_condition"] = "timeout" + + if target_tol is None: + self._early_finish_check = self._fixed_end + return + + num_e_ops = len(self._raw_ops) + + if not num_e_ops: + raise ValueError("Cannot target a tolerance without e_ops") + + self._estimated_ntraj = ntraj + + targets = np.array(target_tol) + if targets.ndim == 0: + self._target_tols = np.array([(target_tol, 0.0)] * num_e_ops) + elif targets.shape == (2,): + self._target_tols = np.ones((num_e_ops, 2)) * targets + elif targets.shape == (num_e_ops, 2): + self._target_tols = targets + else: + raise ValueError( + "target_tol must be a number, a pair of (atol, " + "rtol) or a list of (atol, rtol) for each e_ops" + ) + + self._early_finish_check = self._target_tolerance_end + + @property + def runs_states(self): + """ + States of every runs as ``states[run][t]``. + """ + if self.trajectories and self.trajectories[0].states: + return [traj.states for traj in self.trajectories] + else: + return None + + @property + def average_states(self): + """ + States averages as density matrices. + """ + + trajectory_states_available = (self.trajectories and + self.trajectories[0].states) + need_to_reduce_states = False + if self._sum_abs and not self._sum_abs.sum_states: + if not trajectory_states_available: + return None + self._sum_abs._initialize_sum_states(self.trajectories[0]) + need_to_reduce_states = True + if self._sum_rel and not self._sum_rel.sum_states: + if not trajectory_states_available: + return None + self._sum_rel._initialize_sum_states(self.trajectories[0]) + need_to_reduce_states = True + if need_to_reduce_states: + for trajectory in self.trajectories: + self._reduce_states(trajectory) + + if self._sum_abs and self._sum_rel: + return [a + r / self._num_rel_trajectories for a, r in zip( + self._sum_abs.sum_states, self._sum_rel.sum_states) + ] + if self._sum_rel: + return [r / self._num_rel_trajectories + for r in self._sum_rel.sum_states] + return self._sum_abs.sum_states + + @property + def states(self): + """ + Runs final states if available, average otherwise. + """ + return self.runs_states or self.average_states + + @property + def runs_final_states(self): + """ + Last states of each trajectories. + """ + if self.trajectories and self.trajectories[0].final_state: + return [traj.final_state for traj in self.trajectories] + else: + return None + + @property + def average_final_state(self): + """ + Last states of each trajectories averaged into a density matrix. + """ + if ((self._sum_abs and not self._sum_abs.sum_final_state) or + (self._sum_rel and not self._sum_rel.sum_final_state)): + if (average_states := self.average_states) is not None: + return average_states[-1] + return None + + if self._sum_abs and self._sum_rel: + return (self._sum_abs.sum_final_state + + self._sum_rel.sum_final_state / self._num_rel_trajectories) + if self._sum_rel: + return self._sum_rel.sum_final_state / self._num_rel_trajectories + return self._sum_abs.sum_final_state + + @property + def final_state(self): + """ + Runs final states if available, average otherwise. + """ + return self.runs_final_states or self.average_final_state + + @property + def average_expect(self): + return [np.array(val) for val in self.average_e_data.values()] + + @property + def std_expect(self): + return [np.array(val) for val in self.std_e_data.values()] + + @property + def runs_expect(self): + return [np.array(val) for val in self.runs_e_data.values()] + + @property + def expect(self): + return [np.array(val) for val in self.e_data.values()] + + @property + def e_data(self): + return self.runs_e_data or self.average_e_data + + @property + def runs_weights(self): + result = [] + if self._weight_info: + for w, isabs in self._weight_info: + result.append(w if isabs else w / self._num_rel_trajectories) + else: + for traj in self.trajectories: + w = traj.total_weight + isabs = traj.has_absolute_weight + result.append(w if isabs else w / self._num_rel_trajectories) + return result + + def steady_state(self, N=0): + """ + Average the states of the last ``N`` times of every runs as a density + matrix. Should converge to the steady state in the right circumstances. + + Parameters + ---------- + N : int [optional] + Number of states from the end of ``tlist`` to average. Per default + all states will be averaged. + """ + N = int(N) or len(self.times) + N = len(self.times) if N > len(self.times) else N + states = self.average_states + if states is not None: + return sum(states[-N:]) / N + else: + return None + + def __repr__(self): + lines = [ + f"<{self.__class__.__name__}", + f" Solver: {self.solver}", + ] + if self.stats: + lines.append(" Solver stats:") + lines.extend(f" {k}: {v!r}" for k, v in self.stats.items()) + if self.times: + lines.append( + f" Time interval: [{self.times[0]}, {self.times[-1]}]" + f" ({len(self.times)} steps)" + ) + lines.append(f" Number of e_ops: {len(self.e_data)}") + if self.states: + lines.append(" States saved.") + elif self.final_state is not None: + lines.append(" Final state saved.") + else: + lines.append(" State not saved.") + lines.append(f" Number of trajectories: {self.num_trajectories}") + if self.trajectories: + lines.append(" Trajectories saved.") + else: + lines.append(" Trajectories not saved.") + lines.append(">") + return "\n".join(lines) + + def merge(self, other, p=None): + r""" + Merges two multi-trajectory results. + + If this result represent an ensemble :math:`\rho`, and `other` + represents an ensemble :math:`\rho'`, then the merged result + represents the ensemble + + .. math:: + \rho_{\mathrm{merge}} = p \rho + (1 - p) \rho' + + where p is a parameter between 0 and 1. Its default value is + :math:`p_{\textrm{def}} = N / (N + N')`, N and N' being the number of + trajectories in the two result objects. (In the case of weighted + trajectories, only trajectories without absolute weights are counted.) + + Parameters + ---------- + other : MultiTrajResult + The multi-trajectory result to merge with this one + p : float [optional] + The relative weight of this result in the combination. By default, + will be chosen such that all trajectories contribute equally + to the merged result. + """ + if not isinstance(other, MultiTrajResult): + return NotImplemented + if self._raw_ops != other._raw_ops: + raise ValueError("Shared `e_ops` is required to merge results") + if self.times != other.times: + raise ValueError("Shared `times` are is required to merge results") + + new = self.__class__( + self._raw_ops, self.options, solver=self.solver, stats=self.stats + ) + new.times = self.times + new.e_ops = self.e_ops + + new.num_trajectories = self.num_trajectories + other.num_trajectories + new._num_rel_trajectories = (self._num_rel_trajectories + + other._num_rel_trajectories) + new.seeds = self.seeds + other.seeds + + p_equal = self._num_rel_trajectories / new._num_rel_trajectories + if p is None: + p = p_equal + + if self.trajectories and other.trajectories: + new.trajectories = self._merge_trajectories(other, p, p_equal) + else: + new._weight_info = self._merge_weight_info(other, p, p_equal) + + new._sum_abs = _TrajectorySum.merge( + self._sum_abs, p, other._sum_abs, 1 - p) + new._sum_rel = _TrajectorySum.merge( + self._sum_rel, p / p_equal, + other._sum_rel, (1 - p) / (1 - p_equal)) + + new._create_e_data() + + if self.runs_e_data and other.runs_e_data: + new.runs_e_data = {} + for k in self._raw_ops: + new.runs_e_data[k] = self.runs_e_data[k] + other.runs_e_data[k] + + new.stats["run time"] += other.stats["run time"] + new.stats["end_condition"] = "Merged results" + + return new + + def _merge_weight(self, p, p_equal, isabs): + """ + Merging two result objects can make the trajectories pick up + merge weights. In order to have + rho_merge = p * rho1 + (1-p) * rho2, + the merge weights must be as defined here. The merge weight depends on + whether that trajectory has an absolute weight (`isabs`). The parameter + `p_equal` is the value of p where all trajectories contribute equally. + """ + if isabs: + return p + return p / p_equal + + def _merge_weight_info(self, other, p, p_equal): + new_weight_info = [] + + if self._weight_info: + for w, isabs in self._weight_info: + new_weight_info.append( + (w * self._merge_weight(p, p_equal, isabs), isabs) + ) + else: + for traj in self.trajectories: + w = traj.total_weight + isabs = traj.has_absolute_weight + new_weight_info.append( + (w * self._merge_weight(p, p_equal, isabs), isabs) + ) + + if other._weight_info: + for w, isabs in other._weight_info: + new_weight_info.append( + (w * self._merge_weight(1 - p, 1 - p_equal, isabs), isabs) + ) + else: + for traj in other.trajectories: + w = traj.total_weight + isabs = traj.has_absolute_weight + new_weight_info.append( + (w * self._merge_weight(1 - p, 1 - p_equal, isabs), isabs) + ) + + return new_weight_info + + def _merge_trajectories(self, other, p, p_equal): + if (p == p_equal and + self.num_trajectories == self._num_rel_trajectories and + other.num_trajectories == other._num_rel_trajectories): + return self.trajectories + other.trajectories + + result = [] + for traj in self.trajectories: + if (mweight := self._merge_weight( + p, p_equal, traj.has_absolute_weight)) != 1: + traj = copy(traj) + traj.add_relative_weight(mweight) + result.append(traj) + for traj in other.trajectories: + if (mweight := self._merge_weight( + 1 - p, 1 - p_equal, traj.has_absolute_weight)) != 1: + traj = copy(traj) + traj.add_relative_weight(mweight) + result.append(traj) + return result + + def __add__(self, other): + return self.merge(other, p=None) + + +class _TrajectorySum: + def __init__(self, example_trajectory, store_states, store_final_state): + if example_trajectory.states and store_states: + self._initialize_sum_states(example_trajectory) + else: + self.sum_states = None + + if (fstate := example_trajectory.final_state) and store_final_state: + self.sum_final_state = qzero_like(_to_dm(fstate)) + else: + self.sum_final_state = None + + self.sum_expect = [ + np.zeros_like(expect) for expect in example_trajectory.expect + ] + self.sum2_expect = [ + np.zeros_like(expect) for expect in example_trajectory.expect + ] + + def _initialize_sum_states(self, example_trajectory): + self.sum_states = [ + qzero_like(_to_dm(state)) for state in example_trajectory.states] + + def reduce_states(self, trajectory): + if trajectory.has_weight: + self.sum_states = [ + accu + weight * _to_dm(state) + for accu, state, weight in zip(self.sum_states, + trajectory.states, + trajectory._total_weight_tlist) + ] + else: + self.sum_states = [ + accu + _to_dm(state) + for accu, state in zip(self.sum_states, trajectory.states) + ] + + def reduce_final_state(self, trajectory): + if trajectory.has_weight: + self.sum_final_state += (trajectory._final_weight * + _to_dm(trajectory.final_state)) + else: + self.sum_final_state += _to_dm(trajectory.final_state) + + def reduce_expect(self, trajectory): + weight = trajectory.total_weight + for i, expect_traj in enumerate(trajectory.expect): + self.sum_expect[i] += weight * expect_traj + self.sum2_expect[i] += weight * expect_traj**2 + + @staticmethod + def merge(sum1, weight1, sum2, weight2): + if sum1 is None and sum2 is None: + return None + if sum1 is None: + return _TrajectorySum.merge(sum2, weight2, sum1, weight1) + + new = copy(sum1) + + if sum2 is None: + if sum1.sum_states: + new.sum_states = [ + weight1 * state1 for state1 in sum1.sum_states + ] + if sum1.sum_final_state: + new.sum_final_state = weight1 * sum1.sum_final_state + new.sum_expect = [weight1 * e1 for e1 in sum1.sum_expect] + new.sum2_expect = [weight1 * e1 for e1 in sum1.sum2_expect] + return new + + if sum1.sum_states and sum2.sum_states: + new.sum_states = [ + weight1 * state1 + weight2 * state2 for state1, state2 in zip( + sum1.sum_states, sum2.sum_states + ) + ] + else: + new.sum_states = None + + if sum1.sum_final_state and sum2.sum_final_state: + new.sum_final_state = ( + weight1 * sum1.sum_final_state + + weight2 * sum2.sum_final_state) + else: + new.sum_final_state = None + + new.sum_expect = [weight1 * e1 + weight2 * e2 for e1, e2 in zip( + sum1.sum_expect, sum2.sum_expect) + ] + new.sum2_expect = [weight1 * e1 + weight2 * e2 for e1, e2 in zip( + sum1.sum2_expect, sum2.sum2_expect) + ] + + return new + + +class McResult(MultiTrajResult): + """ + Class for storing Monte-Carlo solver results. + + Parameters + ---------- + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these + The ``e_ops`` parameter defines the set of values to record at + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that + operator given the state at ``t``. If the element is a function, ``f``, + the value recorded is ``f(t, state)``. + + The values are recorded in the ``.expect`` attribute of this result + object. ``.expect`` is a list, where each item contains the values + of the corresponding ``e_op``. + + options : :obj:`~SolverResultsOptions` + The options for this result class. + + solver : str or None + The name of the solver generating these results. + + stats : dict + The stats generated by the solver while producing these results. Note + that the solver may update the stats directly while producing results. + Must include a value for "num_collapse". + + kw : dict + Additional parameters specific to a result sub-class. + + Attributes + ---------- + collapse : list + For each run, a list of every collapse as a tuple of the time it + happened and the corresponding ``c_ops`` index. + """ + + # Collapse are only produced by mcsolve. + def _add_collapse(self, trajectory): + self.collapse.append(trajectory.collapse) + if trajectory.has_time_dependent_weight: + self._time_dependent_weights = True + + def _post_init(self): + super()._post_init() + self.num_c_ops = self.stats["num_collapse"] + self._time_dependent_weights = False + self.collapse = [] + self.add_processor(self._add_collapse) + + @property + def col_times(self): + """ + List of the times of the collapses for each runs. + """ + out = [] + for col_ in self.collapse: + col = list(zip(*col_)) + col = [] if len(col) == 0 else col[0] + out.append(col) + return out + + @property + def col_which(self): + """ + List of the indexes of the collapses for each runs. + """ + out = [] + for col_ in self.collapse: + col = list(zip(*col_)) + col = [] if len(col) == 0 else col[1] + out.append(col) + return out + + @property + def photocurrent(self): + """ + Average photocurrent or measurement of the evolution. + """ + if self._time_dependent_weights: + raise NotImplementedError("photocurrent is not implemented " + "for this solver.") + + collapse_times = [[] for _ in range(self.num_c_ops)] + collapse_weights = [[] for _ in range(self.num_c_ops)] + tlist = self.times + for collapses, weight in zip(self.collapse, self.runs_weights): + for t, which in collapses: + collapse_times[which].append(t) + collapse_weights[which].append(weight) + + mesurement = [ + np.histogram(times, bins=tlist, weights=weights)[0] + / np.diff(tlist) + for times, weights in zip(collapse_times, collapse_weights) + ] + return mesurement + + @property + def runs_photocurrent(self): + """ + Photocurrent or measurement of each runs. + """ + if self._time_dependent_weights: + raise NotImplementedError("runs_photocurrent is not implemented " + "for this solver.") + + tlist = self.times + measurements = [] + for collapses in self.collapse: + collapse_times = [[] for _ in range(self.num_c_ops)] + for t, which in collapses: + collapse_times[which].append(t) + measurements.append( + [ + np.histogram(times, tlist)[0] / np.diff(tlist) + for times in collapse_times + ] + ) + return measurements + + def merge(self, other, p=None): + new = super().merge(other, p) + new.collapse = self.collapse + other.collapse + new._time_dependent_weights = ( + self._time_dependent_weights or other._time_dependent_weights) + return new + + +class NmmcResult(McResult): + """ + Class for storing the results of the non-Markovian Monte-Carlo solver. + + Parameters + ---------- + e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these + The ``e_ops`` parameter defines the set of values to record at + each time step ``t``. If an element is a :obj:`.Qobj` or + :obj:`.QobjEvo` the value recorded is the expectation value of that + operator given the state at ``t``. If the element is a function, ``f``, + the value recorded is ``f(t, state)``. + + The values are recorded in the ``.expect`` attribute of this result + object. ``.expect`` is a list, where each item contains the values + of the corresponding ``e_op``. + + options : :obj:`~SolverResultsOptions` + The options for this result class. + + solver : str or None + The name of the solver generating these results. + + stats : dict + The stats generated by the solver while producing these results. Note + that the solver may update the stats directly while producing results. + Must include a value for "num_collapse". + + kw : dict + Additional parameters specific to a result sub-class. + + Attributes + ---------- + average_trace : list + The average trace (i.e., averaged over all trajectories) at each time. + + std_trace : list + The standard deviation of the trace at each time. + + runs_trace : list of lists + For each recorded trajectory, the trace at each time. + Only present if ``keep_runs_results`` is set in the options. + """ + + def _post_init(self): + super()._post_init() + + self._sum_trace_abs = None + self._sum_trace_rel = None + self._sum2_trace_abs = None + self._sum2_trace_rel = None + + self.average_trace = [] + self.std_trace = [] + self.runs_trace = [] + + self.add_processor(self._add_trace) + + def _add_first_traj(self, trajectory): + super()._add_first_traj(trajectory) + self._sum_trace_abs = np.zeros_like(trajectory.trace) + self._sum_trace_rel = np.zeros_like(trajectory.trace) + self._sum2_trace_abs = np.zeros_like(trajectory.trace) + self._sum2_trace_rel = np.zeros_like(trajectory.trace) + + def _add_trace(self, trajectory): + if trajectory.has_absolute_weight: + self._sum_trace_abs += trajectory._total_weight_tlist + self._sum2_trace_abs += np.abs(trajectory._total_weight_tlist) ** 2 + else: + self._sum_trace_rel += trajectory._total_weight_tlist + self._sum2_trace_rel += np.abs(trajectory._total_weight_tlist) ** 2 + + self._compute_avg_trace() + if self.options["keep_runs_results"]: + self.runs_trace.append(trajectory.trace) + + def _compute_avg_trace(self): + avg = self._sum_trace_abs + if self._num_rel_trajectories > 0: + avg = avg + self._sum_trace_rel / self._num_rel_trajectories + avg2 = self._sum2_trace_abs + if self._num_rel_trajectories > 0: + avg2 = avg2 + self._sum2_trace_rel / self._num_rel_trajectories + + self.average_trace = avg + self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) + + @property + def trace(self): + """ + Refers to ``average_trace`` or ``runs_trace``, depending on whether + ``keep_runs_results`` is set in the options. + """ + return self.runs_trace or self.average_trace + + def merge(self, other, p=None): + new = super().merge(other, p) + + p_eq = self._num_rel_trajectories / new._num_rel_trajectories + if p is None: + p = p_eq + + new._sum_trace_abs = ( + self._merge_weight(p, p_eq, True) * self._sum_trace_abs + + self._merge_weight(1 - p, 1 - p_eq, True) * other._sum_trace_abs + ) + new._sum2_trace_abs = ( + self._merge_weight(p, p_eq, True) * self._sum2_trace_abs + + self._merge_weight(1 - p, 1 - p_eq, True) * other._sum2_trace_abs + ) + new._sum_trace_rel = ( + self._merge_weight(p, p_eq, False) * self._sum_trace_rel + + self._merge_weight(1 - p, 1 - p_eq, False) * other._sum_trace_rel + ) + new._sum2_trace_rel = ( + self._merge_weight(p, p_eq, False) * self._sum2_trace_rel + + self._merge_weight(1 - p, 1 - p_eq, False) * other._sum2_trace_rel + ) + new._compute_avg_trace() + + if self.runs_trace and other.runs_trace: + new.runs_trace = self.runs_trace + other.runs_trace + + return new + + +def _to_dm(state): + if state.type == "ket": + state = state.proj() + return state diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 397873a88c..734763d07c 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -5,8 +5,8 @@ import numpy as np import scipy -from .result import NmmcResult from .multitraj import MultiTrajSolver +from .multitrajresult import NmmcResult from .mcsolve import MCSolver, MCIntegrator from .mesolve import MESolver, mesolve from .cy.nm_mcsolve import RateShiftCoefficient, SqrtRealCoefficient diff --git a/qutip/solver/result.py b/qutip/solver/result.py index 923b377c3c..fb43a75cff 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1,16 +1,13 @@ """ Class for solve function results""" -from copy import copy from typing import TypedDict import numpy as np -from ..core import Qobj, QobjEvo, expect, qzero_like + +from ..core import Qobj, QobjEvo, expect __all__ = [ "Result", - "MultiTrajResult", "TrajectoryResult", - "McResult", - "NmmcResult", ] @@ -367,844 +364,6 @@ def final_state(self): return None -class MultiTrajResultOptions(TypedDict): - store_states: bool - store_final_state: bool - keep_runs_results: bool - - -class MultiTrajResult(_BaseResult): - """ - Base class for storing results for solver using multiple trajectories. - - Parameters - ---------- - e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these - The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`.Qobj` or - :obj:`.QobjEvo` the value recorded is the expectation value of that - operator given the state at ``t``. If the element is a function, ``f``, - the value recorded is ``f(t, state)``. - - The values are recorded in the ``.expect`` attribute of this result - object. ``.expect`` is a list, where each item contains the values - of the corresponding ``e_op``. - - Function ``e_ops`` must return a number so the average can be computed. - - options : dict - The options for this result class. - - solver : str or None - The name of the solver generating these results. - - stats : dict or None - The stats generated by the solver while producing these results. Note - that the solver may update the stats directly while producing results. - - kw : dict - Additional parameters specific to a result sub-class. - - Attributes - ---------- - times : list - A list of the times at which the expectation values and states were - recorded. - - average_states : list of :obj:`.Qobj` - The state at each time ``t`` (if the recording of the state was - requested) averaged over all trajectories as a density matrix. - - runs_states : list of list of :obj:`.Qobj` - The state for each trajectory and each time ``t`` (if the recording of - the states and trajectories was requested) - - final_state : :obj:`.Qobj`: - The final state (if the recording of the final state was requested) - averaged over all trajectories as a density matrix. - - runs_final_state : list of :obj:`.Qobj` - The final state for each trajectory (if the recording of the final - state and trajectories was requested). - - average_expect : list of array of expectation values - A list containing the values of each ``e_op`` averaged over each - trajectories. The list is in the same order in which the ``e_ops`` were - supplied and empty if no ``e_ops`` were given. - - Each element is itself an array and contains the values of the - corresponding ``e_op``, with one value for each time in ``.times``. - - std_expect : list of array of expectation values - A list containing the standard derivation of each ``e_op`` over each - trajectories. The list is in the same order in which the ``e_ops`` were - supplied and empty if no ``e_ops`` were given. - - Each element is itself an array and contains the values of the - corresponding ``e_op``, with one value for each time in ``.times``. - - runs_expect : list of array of expectation values - A list containing the values of each ``e_op`` for each trajectories. - The list is in the same order in which the ``e_ops`` were - supplied and empty if no ``e_ops`` were given. Only available if the - storing of trajectories was requested. - - The order of the elements is ``runs_expect[e_ops][trajectory][time]``. - - Each element is itself an array and contains the values of the - corresponding ``e_op``, with one value for each time in ``.times``. - - average_e_data : dict - A dictionary containing the values of each ``e_op`` averaged over each - trajectories. If the ``e_ops`` were supplied as a dictionary, the keys - are the same as in that dictionary. Otherwise the keys are the index of - the ``e_op`` in the ``.expect`` list. - - The lists of expectation values returned are the *same* lists as - those returned by ``.expect``. - - average_e_data : dict - A dictionary containing the standard derivation of each ``e_op`` over - each trajectories. If the ``e_ops`` were supplied as a dictionary, the - keys are the same as in that dictionary. Otherwise the keys are the - index of the ``e_op`` in the ``.expect`` list. - - The lists of expectation values returned are the *same* lists as - those returned by ``.expect``. - - runs_e_data : dict - A dictionary containing the values of each ``e_op`` for each - trajectories. If the ``e_ops`` were supplied as a dictionary, the keys - are the same as in that dictionary. Otherwise the keys are the index of - the ``e_op`` in the ``.expect`` list. Only available if the storing - of trajectories was requested. - - The order of the elements is ``runs_expect[e_ops][trajectory][time]``. - - The lists of expectation values returned are the *same* lists as - those returned by ``.expect``. - - runs_weights : list - For each trajectory, the weight with which that trajectory enters - averages. - - solver : str or None - The name of the solver generating these results. - - stats : dict or None - The stats generated by the solver while producing these results. - - options : :obj:`~SolverResultsOptions` - The options for this result class. - """ - - options: MultiTrajResultOptions - - def __init__( - self, e_ops, options: MultiTrajResultOptions, *, - solver=None, stats=None, **kw, - ): - super().__init__(options, solver=solver, stats=stats) - self._raw_ops = self._e_ops_to_dict(e_ops) - - self.trajectories = [] - self.num_trajectories = 0 - self.seeds = [] - - self.average_e_data = {} - self.std_e_data = {} - if self.options["keep_runs_results"]: - self.runs_e_data = {k: [] for k in self._raw_ops} - else: - self.runs_e_data = {} - - # Will be initialized at the first trajectory - self.times = None - self.e_ops = None - - # We separate all sums into terms of trajectories with specified - # absolute weight (_abs) or without (_rel). They will be initialized - # when the first trajectory of the respective type is added. - self._sum_rel = None - self._sum_abs = None - # Number of trajectories without specified absolute weight - self._num_rel_trajectories = 0 - # Needed for merging results - self._weight_info = [] - # Needed for target tolerance computation - self._total_abs_weight = np.array(0) - - self._post_init(**kw) - - @property - def _store_average_density_matrices(self) -> bool: - return ( - self.options["store_states"] - or (self.options["store_states"] is None and self._raw_ops == {}) - ) and not self.options["keep_runs_results"] - - @property - def _store_final_density_matrix(self) -> bool: - return ( - self.options["store_final_state"] - and not self._store_average_density_matrices - and not self.options["keep_runs_results"] - ) - - def _add_first_traj(self, trajectory): - """ - Read the first trajectory, intitializing needed data. - """ - self.times = trajectory.times - self.e_ops = trajectory.e_ops - - def _store_trajectory(self, trajectory): - self.trajectories.append(trajectory) - - def _store_weight_info(self, trajectory): - if trajectory.has_absolute_weight: - self._total_abs_weight = ( - self._total_abs_weight + trajectory.total_weight) - if len(self.trajectories) == 0: - # store weight info only if trajectories are not stored - self._weight_info.append( - (trajectory.total_weight, trajectory.has_absolute_weight)) - - def _reduce_states(self, trajectory): - if trajectory.has_absolute_weight: - self._sum_abs.reduce_states(trajectory) - else: - self._sum_rel.reduce_states(trajectory) - - def _reduce_final_state(self, trajectory): - if trajectory.has_absolute_weight: - self._sum_abs.reduce_final_state(trajectory) - else: - self._sum_rel.reduce_final_state(trajectory) - - def _reduce_expect(self, trajectory): - """ - Compute the average of the expectation values and store it in it's - multiple formats. - """ - if trajectory.has_absolute_weight: - self._sum_abs.reduce_expect(trajectory) - else: - self._sum_rel.reduce_expect(trajectory) - - self._create_e_data() - - if self.runs_e_data: - for k in self._raw_ops: - self.runs_e_data[k].append(trajectory.e_data[k]) - - def _create_e_data(self): - for i, k in enumerate(self._raw_ops): - avg = 0 - avg2 = 0 - if self._sum_abs: - avg += self._sum_abs.sum_expect[i] - avg2 += self._sum_abs.sum2_expect[i] - if self._sum_rel: - avg += ( - self._sum_rel.sum_expect[i] / self._num_rel_trajectories - ) - avg2 += ( - self._sum_rel.sum2_expect[i] / self._num_rel_trajectories - ) - - self.average_e_data[k] = list(avg) - # mean(expect**2) - mean(expect)**2 can something be very small - # negative (-1e-15) which raise an error for float sqrt. - self.std_e_data[k] = list(np.sqrt(np.abs(avg2 - np.abs(avg**2)))) - - def _increment_traj(self, trajectory): - if self.num_trajectories == 0: - self._add_first_traj(trajectory) - - if trajectory.has_absolute_weight: - if self._sum_abs is None: - self._sum_abs = _TrajectorySum( - trajectory, - self._store_average_density_matrices, - self._store_final_density_matrix) - else: - self._num_rel_trajectories += 1 - if self._sum_rel is None: - self._sum_rel = _TrajectorySum( - trajectory, - self._store_average_density_matrices, - self._store_final_density_matrix) - - self.num_trajectories += 1 - - def _no_end(self): - """ - Remaining number of trajectories needed to finish cannot be determined - by this object. - """ - return np.inf - - def _fixed_end(self): - """ - Finish at a known number of trajectories. - """ - ntraj_left = self._target_ntraj - self.num_trajectories - if ntraj_left == 0: - self.stats["end_condition"] = "ntraj reached" - return ntraj_left - - def _average_computer(self): - avg = np.array(self._sum_rel.sum_expect) / self._num_rel_trajectories - avg2 = np.array(self._sum_rel.sum2_expect) / self._num_rel_trajectories - return avg, avg2 - - def _target_tolerance_end(self): - """ - Compute the error on the expectation values using jackknife resampling. - Return the approximate number of trajectories needed to have this - error within the tolerance fot all e_ops and times. - """ - if self.num_trajectories >= self._target_ntraj: - # First make sure that "ntraj" setting is always respected - self.stats["end_condition"] = "ntraj reached" - return 0 - - if self._num_rel_trajectories <= 1: - return np.inf - avg, avg2 = self._average_computer() - target = np.array( - [ - atol + rtol * mean - for mean, (atol, rtol) in zip(avg, self._target_tols) - ] - ) - - one = np.array(1) - if self._num_rel_trajectories < self.num_trajectories: - # We only include traj. without abs. weights in this calculation. - # Since there are traj. with abs. weights., the weights don't add - # up to one. We have to consider that as follows: - # <(x - )^2> / <1> = / <1> - ^2 / <1>^2 - # and "<1>" is one minus the sum of all absolute weights - one = one - self._total_abs_weight - - target_ntraj = np.max((avg2 / one - (abs(avg) ** 2) / (one ** 2)) / - target**2 + 1) - - self._estimated_ntraj = min(target_ntraj - self._num_rel_trajectories, - self._target_ntraj - self.num_trajectories) - if self._estimated_ntraj <= 0: - self.stats["end_condition"] = "target tolerance reached" - return self._estimated_ntraj - - def _post_init(self): - self._target_ntraj = None - self._target_tols = None - self._early_finish_check = self._no_end - - self.add_processor(self._increment_traj) - store_trajectory = self.options["keep_runs_results"] - if store_trajectory: - self.add_processor(self._store_trajectory) - if self._store_average_density_matrices: - self.add_processor(self._reduce_states) - if self._store_final_density_matrix: - self.add_processor(self._reduce_final_state) - if self._raw_ops: - self.add_processor(self._reduce_expect) - self.add_processor(self._store_weight_info) - - self.stats["end_condition"] = "unknown" - - def add(self, trajectory_info): - """ - Add a trajectory to the evolution. - - Trajectories can be saved or average canbe extracted depending on the - options ``keep_runs_results``. - - Parameters - ---------- - trajectory_info : tuple of seed and trajectory - - seed: int, SeedSequence - Seed used to generate the trajectory. - - trajectory : :class:`Result` - Run result for one evolution over the times. - - Returns - ------- - remaing_traj : number - Return the number of trajectories still needed to reach the target - tolerance. If no tolerance is provided, return infinity. - """ - seed, trajectory = trajectory_info - self.seeds.append(seed) - - if not isinstance(trajectory, TrajectoryResult): - trajectory.has_weight = False - trajectory.has_absolute_weight = False - trajectory.has_time_dependent_weight = False - trajectory.total_weight = 1 - - for op in self._state_processors: - op(trajectory) - - return self._early_finish_check() - - def add_end_condition(self, ntraj, target_tol=None): - """ - Set the condition to stop the computing trajectories when the certain - condition are fullfilled. - Supported end condition for multi trajectories computation are: - - - Reaching a number of trajectories. - - Error bar on the expectation values reach smaller than a given - tolerance. - - Parameters - ---------- - ntraj : int - Number of trajectories expected. - - target_tol : float, array_like, [optional] - Target tolerance of the evolution. The evolution will compute - trajectories until the error on the expectation values is lower - than this tolerance. The error is computed using jackknife - resampling. ``target_tol`` can be an absolute tolerance, a pair of - absolute and relative tolerance, in that order. Lastly, it can be a - list of pairs of (atol, rtol) for each e_ops. - - Error estimation is done with jackknife resampling. - """ - self._target_ntraj = ntraj - self.stats["end_condition"] = "timeout" - - if target_tol is None: - self._early_finish_check = self._fixed_end - return - - num_e_ops = len(self._raw_ops) - - if not num_e_ops: - raise ValueError("Cannot target a tolerance without e_ops") - - self._estimated_ntraj = ntraj - - targets = np.array(target_tol) - if targets.ndim == 0: - self._target_tols = np.array([(target_tol, 0.0)] * num_e_ops) - elif targets.shape == (2,): - self._target_tols = np.ones((num_e_ops, 2)) * targets - elif targets.shape == (num_e_ops, 2): - self._target_tols = targets - else: - raise ValueError( - "target_tol must be a number, a pair of (atol, " - "rtol) or a list of (atol, rtol) for each e_ops" - ) - - self._early_finish_check = self._target_tolerance_end - - @property - def runs_states(self): - """ - States of every runs as ``states[run][t]``. - """ - if self.trajectories and self.trajectories[0].states: - return [traj.states for traj in self.trajectories] - else: - return None - - @property - def average_states(self): - """ - States averages as density matrices. - """ - - trajectory_states_available = (self.trajectories and - self.trajectories[0].states) - need_to_reduce_states = False - if self._sum_abs and not self._sum_abs.sum_states: - if not trajectory_states_available: - return None - self._sum_abs._initialize_sum_states(self.trajectories[0]) - need_to_reduce_states = True - if self._sum_rel and not self._sum_rel.sum_states: - if not trajectory_states_available: - return None - self._sum_rel._initialize_sum_states(self.trajectories[0]) - need_to_reduce_states = True - if need_to_reduce_states: - for trajectory in self.trajectories: - self._reduce_states(trajectory) - - if self._sum_abs and self._sum_rel: - return [a + r / self._num_rel_trajectories for a, r in zip( - self._sum_abs.sum_states, self._sum_rel.sum_states) - ] - if self._sum_rel: - return [r / self._num_rel_trajectories - for r in self._sum_rel.sum_states] - return self._sum_abs.sum_states - - @property - def states(self): - """ - Runs final states if available, average otherwise. - """ - return self.runs_states or self.average_states - - @property - def runs_final_states(self): - """ - Last states of each trajectories. - """ - if self.trajectories and self.trajectories[0].final_state: - return [traj.final_state for traj in self.trajectories] - else: - return None - - @property - def average_final_state(self): - """ - Last states of each trajectories averaged into a density matrix. - """ - if ((self._sum_abs and not self._sum_abs.sum_final_state) or - (self._sum_rel and not self._sum_rel.sum_final_state)): - if (average_states := self.average_states) is not None: - return average_states[-1] - return None - - if self._sum_abs and self._sum_rel: - return (self._sum_abs.sum_final_state + - self._sum_rel.sum_final_state / self._num_rel_trajectories) - if self._sum_rel: - return self._sum_rel.sum_final_state / self._num_rel_trajectories - return self._sum_abs.sum_final_state - - @property - def final_state(self): - """ - Runs final states if available, average otherwise. - """ - return self.runs_final_states or self.average_final_state - - @property - def average_expect(self): - return [np.array(val) for val in self.average_e_data.values()] - - @property - def std_expect(self): - return [np.array(val) for val in self.std_e_data.values()] - - @property - def runs_expect(self): - return [np.array(val) for val in self.runs_e_data.values()] - - @property - def expect(self): - return [np.array(val) for val in self.e_data.values()] - - @property - def e_data(self): - return self.runs_e_data or self.average_e_data - - @property - def runs_weights(self): - result = [] - if self._weight_info: - for w, isabs in self._weight_info: - result.append(w if isabs else w / self._num_rel_trajectories) - else: - for traj in self.trajectories: - w = traj.total_weight - isabs = traj.has_absolute_weight - result.append(w if isabs else w / self._num_rel_trajectories) - return result - - def steady_state(self, N=0): - """ - Average the states of the last ``N`` times of every runs as a density - matrix. Should converge to the steady state in the right circumstances. - - Parameters - ---------- - N : int [optional] - Number of states from the end of ``tlist`` to average. Per default - all states will be averaged. - """ - N = int(N) or len(self.times) - N = len(self.times) if N > len(self.times) else N - states = self.average_states - if states is not None: - return sum(states[-N:]) / N - else: - return None - - def __repr__(self): - lines = [ - f"<{self.__class__.__name__}", - f" Solver: {self.solver}", - ] - if self.stats: - lines.append(" Solver stats:") - lines.extend(f" {k}: {v!r}" for k, v in self.stats.items()) - if self.times: - lines.append( - f" Time interval: [{self.times[0]}, {self.times[-1]}]" - f" ({len(self.times)} steps)" - ) - lines.append(f" Number of e_ops: {len(self.e_data)}") - if self.states: - lines.append(" States saved.") - elif self.final_state is not None: - lines.append(" Final state saved.") - else: - lines.append(" State not saved.") - lines.append(f" Number of trajectories: {self.num_trajectories}") - if self.trajectories: - lines.append(" Trajectories saved.") - else: - lines.append(" Trajectories not saved.") - lines.append(">") - return "\n".join(lines) - - def merge(self, other, p=None): - r""" - Merges two multi-trajectory results. - - If this result represent an ensemble :math:`\rho`, and `other` - represents an ensemble :math:`\rho'`, then the merged result - represents the ensemble - - .. math:: - \rho_{\mathrm{merge}} = p \rho + (1 - p) \rho' - - where p is a parameter between 0 and 1. Its default value is - :math:`p_{\textrm{def}} = N / (N + N')`, N and N' being the number of - trajectories in the two result objects. (In the case of weighted - trajectories, only trajectories without absolute weights are counted.) - - Parameters - ---------- - other : MultiTrajResult - The multi-trajectory result to merge with this one - p : float [optional] - The relative weight of this result in the combination. By default, - will be chosen such that all trajectories contribute equally - to the merged result. - """ - if not isinstance(other, MultiTrajResult): - return NotImplemented - if self._raw_ops != other._raw_ops: - raise ValueError("Shared `e_ops` is required to merge results") - if self.times != other.times: - raise ValueError("Shared `times` are is required to merge results") - - new = self.__class__( - self._raw_ops, self.options, solver=self.solver, stats=self.stats - ) - new.times = self.times - new.e_ops = self.e_ops - - new.num_trajectories = self.num_trajectories + other.num_trajectories - new._num_rel_trajectories = (self._num_rel_trajectories + - other._num_rel_trajectories) - new.seeds = self.seeds + other.seeds - - p_equal = self._num_rel_trajectories / new._num_rel_trajectories - if p is None: - p = p_equal - - if self.trajectories and other.trajectories: - new.trajectories = self._merge_trajectories(other, p, p_equal) - else: - new._weight_info = self._merge_weight_info(other, p, p_equal) - - new._sum_abs = _TrajectorySum.merge( - self._sum_abs, p, other._sum_abs, 1 - p) - new._sum_rel = _TrajectorySum.merge( - self._sum_rel, p / p_equal, - other._sum_rel, (1 - p) / (1 - p_equal)) - - new._create_e_data() - - if self.runs_e_data and other.runs_e_data: - new.runs_e_data = {} - for k in self._raw_ops: - new.runs_e_data[k] = self.runs_e_data[k] + other.runs_e_data[k] - - new.stats["run time"] += other.stats["run time"] - new.stats["end_condition"] = "Merged results" - - return new - - def _merge_weight(self, p, p_equal, isabs): - """ - Merging two result objects can make the trajectories pick up - merge weights. In order to have - rho_merge = p * rho1 + (1-p) * rho2, - the merge weights must be as defined here. The merge weight depends on - whether that trajectory has an absolute weight (`isabs`). The parameter - `p_equal` is the value of p where all trajectories contribute equally. - """ - if isabs: - return p - return p / p_equal - - def _merge_weight_info(self, other, p, p_equal): - new_weight_info = [] - - if self._weight_info: - for w, isabs in self._weight_info: - new_weight_info.append( - (w * self._merge_weight(p, p_equal, isabs), isabs) - ) - else: - for traj in self.trajectories: - w = traj.total_weight - isabs = traj.has_absolute_weight - new_weight_info.append( - (w * self._merge_weight(p, p_equal, isabs), isabs) - ) - - if other._weight_info: - for w, isabs in other._weight_info: - new_weight_info.append( - (w * self._merge_weight(1 - p, 1 - p_equal, isabs), isabs) - ) - else: - for traj in other.trajectories: - w = traj.total_weight - isabs = traj.has_absolute_weight - new_weight_info.append( - (w * self._merge_weight(1 - p, 1 - p_equal, isabs), isabs) - ) - - return new_weight_info - - def _merge_trajectories(self, other, p, p_equal): - if (p == p_equal and - self.num_trajectories == self._num_rel_trajectories and - other.num_trajectories == other._num_rel_trajectories): - return self.trajectories + other.trajectories - - result = [] - for traj in self.trajectories: - if (mweight := self._merge_weight( - p, p_equal, traj.has_absolute_weight)) != 1: - traj = copy(traj) - traj.add_relative_weight(mweight) - result.append(traj) - for traj in other.trajectories: - if (mweight := self._merge_weight( - 1 - p, 1 - p_equal, traj.has_absolute_weight)) != 1: - traj = copy(traj) - traj.add_relative_weight(mweight) - result.append(traj) - return result - - def __add__(self, other): - return self.merge(other, p=None) - - -class _TrajectorySum: - def __init__(self, example_trajectory, store_states, store_final_state): - if example_trajectory.states and store_states: - self._initialize_sum_states(example_trajectory) - else: - self.sum_states = None - - if (fstate := example_trajectory.final_state) and store_final_state: - self.sum_final_state = qzero_like(_to_dm(fstate)) - else: - self.sum_final_state = None - - self.sum_expect = [ - np.zeros_like(expect) for expect in example_trajectory.expect - ] - self.sum2_expect = [ - np.zeros_like(expect) for expect in example_trajectory.expect - ] - - def _initialize_sum_states(self, example_trajectory): - self.sum_states = [ - qzero_like(_to_dm(state)) for state in example_trajectory.states] - - def reduce_states(self, trajectory): - if trajectory.has_weight: - self.sum_states = [ - accu + weight * _to_dm(state) - for accu, state, weight in zip(self.sum_states, - trajectory.states, - trajectory._total_weight_tlist) - ] - else: - self.sum_states = [ - accu + _to_dm(state) - for accu, state in zip(self.sum_states, trajectory.states) - ] - - def reduce_final_state(self, trajectory): - if trajectory.has_weight: - self.sum_final_state += (trajectory._final_weight * - _to_dm(trajectory.final_state)) - else: - self.sum_final_state += _to_dm(trajectory.final_state) - - def reduce_expect(self, trajectory): - weight = trajectory.total_weight - for i, expect_traj in enumerate(trajectory.expect): - self.sum_expect[i] += weight * expect_traj - self.sum2_expect[i] += weight * expect_traj**2 - - @staticmethod - def merge(sum1, weight1, sum2, weight2): - if sum1 is None and sum2 is None: - return None - if sum1 is None: - return _TrajectorySum.merge(sum2, weight2, sum1, weight1) - - new = copy(sum1) - - if sum2 is None: - if sum1.sum_states: - new.sum_states = [ - weight1 * state1 for state1 in sum1.sum_states - ] - if sum1.sum_final_state: - new.sum_final_state = weight1 * sum1.sum_final_state - new.sum_expect = [weight1 * e1 for e1 in sum1.sum_expect] - new.sum2_expect = [weight1 * e1 for e1 in sum1.sum2_expect] - return new - - if sum1.sum_states and sum2.sum_states: - new.sum_states = [ - weight1 * state1 + weight2 * state2 for state1, state2 in zip( - sum1.sum_states, sum2.sum_states - ) - ] - else: - new.sum_states = None - - if sum1.sum_final_state and sum2.sum_final_state: - new.sum_final_state = ( - weight1 * sum1.sum_final_state + - weight2 * sum2.sum_final_state) - else: - new.sum_final_state = None - - new.sum_expect = [weight1 * e1 + weight2 * e2 for e1, e2 in zip( - sum1.sum_expect, sum2.sum_expect) - ] - new.sum2_expect = [weight1 * e1 + weight2 * e2 for e1, e2 in zip( - sum1.sum2_expect, sum2.sum2_expect) - ] - - return new - - class TrajectoryResult(Result): r""" Result class used for single trajectories in multi-trajectory simulations. @@ -1305,267 +464,4 @@ def _final_weight(self): total_weight = self.total_weight if self.has_time_dependent_weight: return total_weight[-1] - return total_weight - - -class McResult(MultiTrajResult): - """ - Class for storing Monte-Carlo solver results. - - Parameters - ---------- - e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these - The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`.Qobj` or - :obj:`.QobjEvo` the value recorded is the expectation value of that - operator given the state at ``t``. If the element is a function, ``f``, - the value recorded is ``f(t, state)``. - - The values are recorded in the ``.expect`` attribute of this result - object. ``.expect`` is a list, where each item contains the values - of the corresponding ``e_op``. - - options : :obj:`~SolverResultsOptions` - The options for this result class. - - solver : str or None - The name of the solver generating these results. - - stats : dict - The stats generated by the solver while producing these results. Note - that the solver may update the stats directly while producing results. - Must include a value for "num_collapse". - - kw : dict - Additional parameters specific to a result sub-class. - - Attributes - ---------- - collapse : list - For each run, a list of every collapse as a tuple of the time it - happened and the corresponding ``c_ops`` index. - """ - - # Collapse are only produced by mcsolve. - def _add_collapse(self, trajectory): - self.collapse.append(trajectory.collapse) - if trajectory.has_time_dependent_weight: - self._time_dependent_weights = True - - def _post_init(self): - super()._post_init() - self.num_c_ops = self.stats["num_collapse"] - self._time_dependent_weights = False - self.collapse = [] - self.add_processor(self._add_collapse) - - @property - def col_times(self): - """ - List of the times of the collapses for each runs. - """ - out = [] - for col_ in self.collapse: - col = list(zip(*col_)) - col = [] if len(col) == 0 else col[0] - out.append(col) - return out - - @property - def col_which(self): - """ - List of the indexes of the collapses for each runs. - """ - out = [] - for col_ in self.collapse: - col = list(zip(*col_)) - col = [] if len(col) == 0 else col[1] - out.append(col) - return out - - @property - def photocurrent(self): - """ - Average photocurrent or measurement of the evolution. - """ - if self._time_dependent_weights: - raise NotImplementedError("photocurrent is not implemented " - "for this solver.") - - collapse_times = [[] for _ in range(self.num_c_ops)] - collapse_weights = [[] for _ in range(self.num_c_ops)] - tlist = self.times - for collapses, weight in zip(self.collapse, self.runs_weights): - for t, which in collapses: - collapse_times[which].append(t) - collapse_weights[which].append(weight) - - mesurement = [ - np.histogram(times, bins=tlist, weights=weights)[0] - / np.diff(tlist) - for times, weights in zip(collapse_times, collapse_weights) - ] - return mesurement - - @property - def runs_photocurrent(self): - """ - Photocurrent or measurement of each runs. - """ - if self._time_dependent_weights: - raise NotImplementedError("runs_photocurrent is not implemented " - "for this solver.") - - tlist = self.times - measurements = [] - for collapses in self.collapse: - collapse_times = [[] for _ in range(self.num_c_ops)] - for t, which in collapses: - collapse_times[which].append(t) - measurements.append( - [ - np.histogram(times, tlist)[0] / np.diff(tlist) - for times in collapse_times - ] - ) - return measurements - - def merge(self, other, p=None): - new = super().merge(other, p) - new.collapse = self.collapse + other.collapse - new._time_dependent_weights = ( - self._time_dependent_weights or other._time_dependent_weights) - return new - - -class NmmcResult(McResult): - """ - Class for storing the results of the non-Markovian Monte-Carlo solver. - - Parameters - ---------- - e_ops : :obj:`.Qobj`, :obj:`.QobjEvo`, function or list or dict of these - The ``e_ops`` parameter defines the set of values to record at - each time step ``t``. If an element is a :obj:`.Qobj` or - :obj:`.QobjEvo` the value recorded is the expectation value of that - operator given the state at ``t``. If the element is a function, ``f``, - the value recorded is ``f(t, state)``. - - The values are recorded in the ``.expect`` attribute of this result - object. ``.expect`` is a list, where each item contains the values - of the corresponding ``e_op``. - - options : :obj:`~SolverResultsOptions` - The options for this result class. - - solver : str or None - The name of the solver generating these results. - - stats : dict - The stats generated by the solver while producing these results. Note - that the solver may update the stats directly while producing results. - Must include a value for "num_collapse". - - kw : dict - Additional parameters specific to a result sub-class. - - Attributes - ---------- - average_trace : list - The average trace (i.e., averaged over all trajectories) at each time. - - std_trace : list - The standard deviation of the trace at each time. - - runs_trace : list of lists - For each recorded trajectory, the trace at each time. - Only present if ``keep_runs_results`` is set in the options. - """ - - def _post_init(self): - super()._post_init() - - self._sum_trace_abs = None - self._sum_trace_rel = None - self._sum2_trace_abs = None - self._sum2_trace_rel = None - - self.average_trace = [] - self.std_trace = [] - self.runs_trace = [] - - self.add_processor(self._add_trace) - - def _add_first_traj(self, trajectory): - super()._add_first_traj(trajectory) - self._sum_trace_abs = np.zeros_like(trajectory.trace) - self._sum_trace_rel = np.zeros_like(trajectory.trace) - self._sum2_trace_abs = np.zeros_like(trajectory.trace) - self._sum2_trace_rel = np.zeros_like(trajectory.trace) - - def _add_trace(self, trajectory): - if trajectory.has_absolute_weight: - self._sum_trace_abs += trajectory._total_weight_tlist - self._sum2_trace_abs += np.abs(trajectory._total_weight_tlist) ** 2 - else: - self._sum_trace_rel += trajectory._total_weight_tlist - self._sum2_trace_rel += np.abs(trajectory._total_weight_tlist) ** 2 - - self._compute_avg_trace() - if self.options["keep_runs_results"]: - self.runs_trace.append(trajectory.trace) - - def _compute_avg_trace(self): - avg = self._sum_trace_abs - if self._num_rel_trajectories > 0: - avg = avg + self._sum_trace_rel / self._num_rel_trajectories - avg2 = self._sum2_trace_abs - if self._num_rel_trajectories > 0: - avg2 = avg2 + self._sum2_trace_rel / self._num_rel_trajectories - - self.average_trace = avg - self.std_trace = np.sqrt(np.abs(avg2 - np.abs(avg) ** 2)) - - @property - def trace(self): - """ - Refers to ``average_trace`` or ``runs_trace``, depending on whether - ``keep_runs_results`` is set in the options. - """ - return self.runs_trace or self.average_trace - - def merge(self, other, p=None): - new = super().merge(other, p) - - p_eq = self._num_rel_trajectories / new._num_rel_trajectories - if p is None: - p = p_eq - - new._sum_trace_abs = ( - self._merge_weight(p, p_eq, True) * self._sum_trace_abs + - self._merge_weight(1 - p, 1 - p_eq, True) * other._sum_trace_abs - ) - new._sum2_trace_abs = ( - self._merge_weight(p, p_eq, True) * self._sum2_trace_abs + - self._merge_weight(1 - p, 1 - p_eq, True) * other._sum2_trace_abs - ) - new._sum_trace_rel = ( - self._merge_weight(p, p_eq, False) * self._sum_trace_rel + - self._merge_weight(1 - p, 1 - p_eq, False) * other._sum_trace_rel - ) - new._sum2_trace_rel = ( - self._merge_weight(p, p_eq, False) * self._sum2_trace_rel + - self._merge_weight(1 - p, 1 - p_eq, False) * other._sum2_trace_rel - ) - new._compute_avg_trace() - - if self.runs_trace and other.runs_trace: - new.runs_trace = self.runs_trace + other.runs_trace - - return new - - -def _to_dm(state): - if state.type == "ket": - state = state.proj() - return state + return total_weight \ No newline at end of file diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 5626f5e540..67e130efcd 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,8 +1,9 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] +from .multitrajresult import MultiTrajResult from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .sode._noise import PreSetWiener -from .result import MultiTrajResult, Result, ExpectOp +from .result import Result, ExpectOp from .multitraj import _MultiTrajRHS, MultiTrajSolver from .. import Qobj, QobjEvo from ..core.dimensions import Dimensions diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index a9ac348d0f..8e250ba743 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -3,7 +3,6 @@ import qutip from copy import copy from qutip.solver.mcsolve import mcsolve, MCSolver -from qutip.solver.solver_base import Solver def _return_constant(t, args): diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index abaf821763..d99fafa40a 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -2,9 +2,8 @@ import pytest import qutip -from qutip.solver.result import ( - Result, MultiTrajResult, McResult, NmmcResult, TrajectoryResult -) +from qutip.solver.result import Result, TrajectoryResult +from qutip.solver.multitrajresult import MultiTrajResult, McResult, NmmcResult def fill_options(**kwargs): From bf604c3e1eb7af04fd1510512dcbcca039db06ea Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 6 May 2024 14:53:30 +0900 Subject: [PATCH 149/305] Documentation for _TrajectorySum class --- qutip/solver/multitrajresult.py | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/qutip/solver/multitrajresult.py b/qutip/solver/multitrajresult.py index 765a8a1f34..77de65b3fe 100644 --- a/qutip/solver/multitrajresult.py +++ b/qutip/solver/multitrajresult.py @@ -762,6 +762,26 @@ def __add__(self, other): class _TrajectorySum: + """ + Keeps running sums of expectation values, and (if requested) states and + final states, over a set of trajectories as they are added one-by-one. + This is used in the `MultiTrajResult` class, which needs to keep track of + several sums of this type. + + Parameters + ---------- + example_trajectory : :obj:`.Result` + An example trajectory with expectation values and states of the same + shape like for the trajectories that will be added later. The data is + only used for initializing arrays in the correct shape and otherwise + ignored. + + store_states : bool + Whether the states of the trajectories will be summed. + + store_final_state : bool + Whether the final states of the trajectories will be summed. + """ def __init__(self, example_trajectory, store_states, store_final_state): if example_trajectory.states and store_states: self._initialize_sum_states(example_trajectory) @@ -785,6 +805,11 @@ def _initialize_sum_states(self, example_trajectory): qzero_like(_to_dm(state)) for state in example_trajectory.states] def reduce_states(self, trajectory): + """ + Adds the states stored in the given trajectory to the running sum + `sum_states`. Takes account of the trajectory's total weight if + present. + """ if trajectory.has_weight: self.sum_states = [ accu + weight * _to_dm(state) @@ -799,6 +824,11 @@ def reduce_states(self, trajectory): ] def reduce_final_state(self, trajectory): + """ + Adds the final state stored in the given trajectory to the running sum + `sum_final_state`. Takes account of the trajectory's total weight if + present. + """ if trajectory.has_weight: self.sum_final_state += (trajectory._final_weight * _to_dm(trajectory.final_state)) @@ -806,6 +836,11 @@ def reduce_final_state(self, trajectory): self.sum_final_state += _to_dm(trajectory.final_state) def reduce_expect(self, trajectory): + """ + Adds the expectation values, and their squares, that are stored in the + given trajectory to the running sums `sum_expect` and `sum2_expect`. + Takes account of the trajectory's total weight if present. + """ weight = trajectory.total_weight for i, expect_traj in enumerate(trajectory.expect): self.sum_expect[i] += weight * expect_traj @@ -813,6 +848,10 @@ def reduce_expect(self, trajectory): @staticmethod def merge(sum1, weight1, sum2, weight2): + """ + Merges the sums of expectation values, states and final states with + the given weights, i.e., `result = weight1 * sum1 + weight2 * sum2`. + """ if sum1 is None and sum2 is None: return None if sum1 is None: From c48b5509148857bf961aabd7230cfbbfb15dd034 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 6 May 2024 15:31:49 +0900 Subject: [PATCH 150/305] Updated nmmcsolve guide and fixed broken links --- doc/guide/dynamics/dynamics-intro.rst | 12 ++++++------ doc/guide/dynamics/dynamics-nmmonte.rst | 6 +++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/guide/dynamics/dynamics-intro.rst b/doc/guide/dynamics/dynamics-intro.rst index cfdf0c1ce0..4429766860 100644 --- a/doc/guide/dynamics/dynamics-intro.rst +++ b/doc/guide/dynamics/dynamics-intro.rst @@ -45,14 +45,14 @@ quantum systems and indicates the type of object returned by the solver: * - Monte Carlo evolution - :func:`~qutip.solver.mcsolve.mcsolve` - :obj:`~qutip.solver.mcsolve.MCSolver` - - :obj:`~qutip.solver.result.McResult` + - :obj:`~qutip.solver.multitrajresult.McResult` * - Non-Markovian Monte Carlo - :func:`~qutip.solver.nm_mcsolve.nm_mcsolve` - :obj:`~qutip.solver.nm_mcsolve.NonMarkovianMCSolver` - - :obj:`~qutip.solver.result.NmmcResult` + - :obj:`~qutip.solver.multitrajresult.NmmcResult` * - Bloch-Redfield master equation - - :func:`~qutip.solver.mesolve.brmesolve` - - :obj:`~qutip.solver.mesolve.BRSolver` + - :func:`~qutip.solver.brmesolve.brmesolve` + - :obj:`~qutip.solver.brmesolve.BRSolver` - :obj:`~qutip.solver.result.Result` * - Floquet-Markov master equation - :func:`~qutip.solver.floquet.fmmesolve` @@ -61,11 +61,11 @@ quantum systems and indicates the type of object returned by the solver: * - Stochastic Schrödinger equation - :func:`~qutip.solver.stochastic.ssesolve` - :obj:`~qutip.solver.stochastic.SSESolver` - - :obj:`~qutip.solver.result.MultiTrajResult` + - :obj:`~qutip.solver.multitrajresult.MultiTrajResult` * - Stochastic master equation - :func:`~qutip.solver.stochastic.smesolve` - :obj:`~qutip.solver.stochastic.SMESolver` - - :obj:`~qutip.solver.result.MultiTrajResult` + - :obj:`~qutip.solver.multitrajresult.MultiTrajResult` * - Transfer Tensor Method time-evolution - :func:`~qutip.solver.nonmarkov.transfertensor.ttmsolve` - None diff --git a/doc/guide/dynamics/dynamics-nmmonte.rst b/doc/guide/dynamics/dynamics-nmmonte.rst index fb8c0bcbde..ae040bdde7 100644 --- a/doc/guide/dynamics/dynamics-nmmonte.rst +++ b/doc/guide/dynamics/dynamics-nmmonte.rst @@ -79,6 +79,9 @@ associated jump rates :math:`\Gamma_n(t)\geq0` appropriate for simulation. We conclude with a simple example demonstrating the usage of the ``nm_mcsolve`` function. For more elaborate, physically motivated examples, we refer to the `accompanying tutorial notebook `_. +Note that the example also demonstrates the usage of the ``improved_sampling`` +option (which is explained in the guide for the +:ref:`Monte Carlo Solver`) in ``nm_mcsolve``. .. plot:: @@ -98,10 +101,11 @@ function. For more elaborate, physically motivated examples, we refer to the ops_and_rates = [] ops_and_rates.append([a0.dag(), gamma1]) ops_and_rates.append([a0, gamma2]) + nm_options = {'map': 'parallel', 'improved_sampling': True} MCSol = nm_mcsolve(H, psi0, times, ops_and_rates, args={'kappa': 1.0 / 0.129, 'nth': 0.063}, e_ops=[a0.dag() * a0, a0 * a0.dag()], - options={'map': 'parallel'}, ntraj=2500) + options=nm_options, ntraj=2500) # mesolve integration for comparison d_ops = [[lindblad_dissipator(a0.dag(), a0.dag()), gamma1], From cd96a59e178037013350b796fa0e4e0b709af3c2 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 6 May 2024 15:44:24 +0900 Subject: [PATCH 151/305] Don't fix random seed in test --- qutip/tests/solver/test_results.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index d99fafa40a..b638bc2342 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -293,7 +293,7 @@ def test_McResult(self, dm, include_no_jump, keep_runs_results): [(-0.25 + 1.5 * 0.75) * np.sin(i) for i in range(10)], [(-0.25 + 1.5 * 0.75) * np.sin(i) * qutip.fock_dm(10, i) for i in range(10)], - id='timedep-marting'), + id='timedep-marting-no-jump'), ]) def test_NmmcResult(self, include_no_jump, martingale, result_trace, result_states): @@ -459,10 +459,6 @@ def test_merge_result(self, keep_runs_results): assert bool(merged_res.trajectories) == keep_runs_results assert merged_res.stats["run time"] == 3 - @pytest.fixture(scope='session') - def fix_seed(self): - np.random.seed(1) - def _random_ensemble(self, abs_weights=True, collapse=False, trace=False, time_dep_weights=False, cls=MultiTrajResult): dim = 10 @@ -503,7 +499,6 @@ def _random_ensemble(self, abs_weights=True, collapse=False, trace=False, return res - @pytest.mark.usefixtures('fix_seed') @pytest.mark.parametrize('abs_weights1', [True, False]) @pytest.mark.parametrize('abs_weights2', [True, False]) @pytest.mark.parametrize('p', [0, 0.1, 1, None]) @@ -531,7 +526,6 @@ def test_merge_weights(self, abs_weights1, abs_weights2, p): ensemble1.states, ensemble2.states, merged.states): assert state == p * state1 + (1 - p) * state2 - @pytest.mark.usefixtures('fix_seed') @pytest.mark.parametrize('p', [0, 0.1, 1, None]) def test_merge_mcresult(self, p): ensemble1 = self._random_ensemble(collapse=True, @@ -553,7 +547,6 @@ def test_merge_mcresult(self, p): merged.photocurrent): np.testing.assert_almost_equal(c, p * c1 + (1 - p) * c2) - @pytest.mark.usefixtures('fix_seed') @pytest.mark.parametrize('p', [0, 0.1, 1, None]) def test_merge_nmmcresult(self, p): ensemble1 = self._random_ensemble( From 7e38492d2303c8ea4e562f3afd116e52387ac180 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 6 May 2024 09:41:09 -0400 Subject: [PATCH 152/305] Syncronize setup.cfg and pyproject.toml requirements --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 896cd3f115..d9f4b90716 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = [ "packaging", "wheel", "cython>=0.29.20; python_version>='3.10'", - "cython>=0.29.20,<3.0.3; python_version<='3.9'", + "cython>=0.29.20,<3.0.0; python_version<='3.9'", # See https://numpy.org/doc/stable/user/depending_on_numpy.html for # the recommended way to build against numpy's C API: "oldest-supported-numpy", From cd85b13838714b55b18b7471d9ba9885be47d55f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 20:41:52 +0000 Subject: [PATCH 153/305] Bump jinja2 from 3.1.3 to 3.1.4 in /doc Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index f8f361dba6..a41a4c36c5 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -11,7 +11,7 @@ idna==3.7 imagesize==1.4.1 ipython==8.11.0 jedi==0.18.2 -Jinja2==3.1.3 +Jinja2==3.1.4 kiwisolver==1.4.4 MarkupSafe==2.1.2 matplotlib==3.7.1 From 3d7e361eee40abb73550edf7f808db6323ee8196 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Tue, 7 May 2024 17:47:42 +0100 Subject: [PATCH 154/305] Refactored dnorm function --- doc/biblio.rst | 5 +- qutip/core/metrics.py | 130 +++++++++++++++++++++--------------------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/doc/biblio.rst b/doc/biblio.rst index 486bf2a963..9ae69b60df 100644 --- a/doc/biblio.rst +++ b/doc/biblio.rst @@ -41,9 +41,8 @@ Bibliography open quantum systems*. :arxiv:`1111.6950` .. [AKN98] - D. Aharonov, A. Kitaev, and N. Nisan, *Quantum circuits with mixed states*, - in Proceedings of the thirtieth annual ACM symposium on Theory of computing, - 20–30 (1998). :arxiv:`quant-ph/9806029` + D. Aharonov, A. Kitaev, and N. Nisan, *Quantum circuits with mixed states*, in Proceedings of the + thirtieth annual ACM symposium on Theory of computing, 20-30 (1998). :arxiv:`quant-ph/9806029` .. [dAless08] D. d’Alessandro, *Introduction to Quantum Control and Dynamics*, (Chapman & Hall/CRC, 2008). diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index f9cb0003bf..2c08049428 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -475,6 +475,9 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, if cvxpy is None: # pragma: no cover raise ImportError("dnorm() requires CVXPY to be installed.") + if B is not None and A.dims != B.dims: + raise TypeError("A and B do not have the same dimensions.") + # We follow the strategy of using Watrous' simpler semidefinite # program in its primal form. This is the same strategy used, # for instance, by both pyGSTi and SchattenNorms.jl. (By contrast, @@ -486,73 +489,70 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, # d between the origin and compelx hull of these. Plugging # this into 2√1-d² gives the diamond norm. + def check_unitary(op): + # Helper function to check not None and is unitary. + if op is None or not op.isoper: + return False + else: + return (op * op.dag() - qeye_like(op)).norm() < 1e-6 + if ( not force_solve - and B is not None - and A.isoper and B.isoper - ): - # Make an identity the same size as A and B to - # compare against. - I = qeye_like(A) - # Compare to B first, so that an error is raised - # as soon as possible. - Bd = B.dag() - if ( - (B * Bd - I).norm() < 1e-6 and - (A * A.dag() - I).norm() < 1e-6 - ): - # Now we are on the fast path, so let's compute the - # eigenvalues, then find the distance between origin and hull - U = A * B.dag() - eigs = U.eigenenergies() - d = _find_poly_distance(eigs) - return 2 * np.sqrt(1 - d**2) # plug into formula - - # Force the input superoperator to be a Choi matrix. - J = to_choi(A) - - if B is not None: - J -= to_choi(B) - - # Watrous 2012 also points out that the diamond norm of Lambda - # is the same as the completely-bounded operator-norm (∞-norm) - # of the dual map of Lambda. We can evaluate that norm much more - # easily if Lambda is completely positive, since then the largest - # eigenvalue is the same as the largest singular value. - - if not force_solve and J.iscp: - S_dual = to_super(J.dual_chan()) - vec_eye = operator_to_vector(qeye(S_dual.dims[1][1])) - op = vector_to_operator(S_dual * vec_eye) - # The 2-norm was not implemented for sparse matrices as of the time - # of this writing. Thus, we must yet again go dense. - return la.norm(op.full(), 2) - - # If we're still here, we need to actually solve the problem. - - # Assume square... - dim = int(np.prod(J.dims[0][0])) - - # Load the parameters with the Choi matrix passed in. - J_dat = _data.to('csr', J.data).as_scipy() - - if not sparse: - problem, Jr, Ji = dnorm_problem(dim) - - # Load the parameters with the Choi matrix passed in. - Jr.value = sp.csr_matrix((J_dat.data.real, J_dat.indices, - J_dat.indptr), - shape=J_dat.shape).toarray() - - Ji.value = sp.csr_matrix((J_dat.data.imag, J_dat.indices, - J_dat.indptr), - shape=J_dat.shape).toarray() - else: - problem = dnorm_sparse_problem(dim, J_dat) - - problem.solve(solver=solver, verbose=verbose) - - return problem.value + and check_unitary(A) + and check_unitary(B) + ): # Special optimisation fo a difference of unitaries. + U = A * B.dag() + eigs = U.eigenenergies() + d = _find_poly_distance(eigs) + value = 2 * np.sqrt(1 - d**2) # plug d into formula + else: # Force the input superoperator to be a Choi matrix. + J = to_choi(A) + if B is not None: + J -= to_choi(B) + + # Watrous 2012 also points out that the diamond norm of Lambda + # is the same as the completely-bounded operator-norm (∞-norm) + # of the dual map of Lambda. We can evaluate that norm much more + # easily if Lambda is completely positive, since then the largest + # eigenvalue is the same as the largest singular value. + + if not force_solve and J.iscp: + S_dual = to_super(J.dual_chan()) + vec_eye = operator_to_vector(qeye(S_dual.dims[1][1])) + op = vector_to_operator(S_dual * vec_eye) + # The 2-norm was not implemented for sparse matrices as of the time + # of this writing. Thus, we must yet again go dense. + value = la.norm(op.full(), 2) + elif not force_solve and J.iscptp: + # diamond norm of a CPTP map is 1 (Prop 3.44 Watrous 2018) + value = 1.0 + else: + # If we're still here, we need to actually solve the problem. + + # Assume square... + dim = int(np.prod(J.dims[0][0])) + + # Load the parameters with the Choi matrix passed in. + J_dat = _data.to('csr', J.data).as_scipy() + + if not sparse: + problem, Jr, Ji = dnorm_problem(dim) + + # Load the parameters with the Choi matrix passed in. + Jr.value = sp.csr_matrix((J_dat.data.real, J_dat.indices, + J_dat.indptr), + shape=J_dat.shape).toarray() + + Ji.value = sp.csr_matrix((J_dat.data.imag, J_dat.indices, + J_dat.indptr), + shape=J_dat.shape).toarray() + else: + problem = dnorm_sparse_problem(dim, J_dat) + + problem.solve(solver=solver, verbose=verbose) + + value = problem.value + return value def unitarity(oper): From 412ea0a0a81c47454f9b12ef628fdb398c92b335 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Tue, 7 May 2024 18:18:57 +0100 Subject: [PATCH 155/305] use built-in isunitary property --- qutip/core/metrics.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index 2c08049428..60e8e1a8a6 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -489,17 +489,11 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, # d between the origin and compelx hull of these. Plugging # this into 2√1-d² gives the diamond norm. - def check_unitary(op): - # Helper function to check not None and is unitary. - if op is None or not op.isoper: - return False - else: - return (op * op.dag() - qeye_like(op)).norm() < 1e-6 - if ( not force_solve - and check_unitary(A) - and check_unitary(B) + and A.isunitary + and B is not None + and B.isunitary ): # Special optimisation fo a difference of unitaries. U = A * B.dag() eigs = U.eigenenergies() From d1e3c31f88b2340079eddedeae3897b472ededb2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 7 May 2024 14:14:41 -0400 Subject: [PATCH 156/305] More stable run_from_experiment test --- qutip/tests/solver/test_stochastic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index eaf2f9c015..95a510c8fe 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -419,13 +419,13 @@ def test_small_step_warnings(method): @pytest.mark.parametrize("method", ["euler", "platen"]) @pytest.mark.parametrize("heterodyne", [True, False]) def test_run_from_experiment_close(method, heterodyne): - N = 10 + N = 5 H = num(N) a = destroy(N) sc_ops = [a, a @ a + (a @ a).dag()] psi0 = basis(N, N-1) - tlist = np.linspace(0, 0.1, 251) + tlist = np.linspace(0, 0.1, 501) options = { "store_measurement": "start", "dt": tlist[1], From 8304a3ab8fe07843c3b3d79b84a5495cb382fc68 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 7 May 2024 15:09:08 -0400 Subject: [PATCH 157/305] Smaller steps in tests feedback --- qutip/tests/solver/test_stochastic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 95a510c8fe..d9c05b280a 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -371,8 +371,8 @@ def func(t, A, W): )] psi0 = basis(N, N-3) - times = np.linspace(0, 10, 101) - options = {"map": "serial", "dt": 0.001} + times = np.linspace(0, 2, 101) + options = {"map": "serial", "dt": 0.0005} solver = SMESolver(H, sc_ops=sc_ops, heterodyne=False, options=options) results = solver.run(psi0, times, e_ops=[num(N)], ntraj=ntraj) From 76bb4cc51b78c209d176644c4ba59dd133fa0104 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Wed, 8 May 2024 09:15:26 +0100 Subject: [PATCH 158/305] Refactoring fixes and docstring clarifications --- qutip/core/metrics.py | 147 ++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 69 deletions(-) diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index 60e8e1a8a6..744d61ec6e 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -434,14 +434,20 @@ def hellinger_dist(A, B, sparse=False, tol=0): def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, sparse=True): - """ + r""" Calculates the diamond norm of the quantum map q_oper, using the simplified semidefinite program of [Wat13]_. The diamond norm SDP is solved by using `CVXPY `_. - If B is provided and both A and B are unitaries, a special optimised - case of the diamond norm is used. See [AKN98]_. + If B is provided and both A and B are unitary, then the diamond norm + of the difference is calculated more efficiently using the following + geometric interpretation: + :math:`\|A - B\|_{\diamond}` equals :math:`2 \sqrt(1 - d^2)`, where + :math:`d`is the distance between the origin and the convex hull of the + eigenvalues of :math:`A B^{\dagger}`. + See [AKN98]_ page 18, in the paragraph immediately below the proof of 12.6, + as a reference. Parameters ---------- @@ -483,11 +489,11 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, # for instance, by both pyGSTi and SchattenNorms.jl. (By contrast, # QETLAB uses the dual problem.) - # Check if A and B are both unitaries. If so we can use a trick - # discussed in D. Aharonov, A. Kitaev, and N. Nisan. (1998). - # In general we can find the eigenvalues of AB⁺ and the distance - # d between the origin and compelx hull of these. Plugging - # this into 2√1-d² gives the diamond norm. + # Check if A and B are both unitaries. If so we can use the geometric + # interpretation mentioned in D. Aharonov, A. Kitaev, and N. Nisan. (1998). + # We find the eigenvalues of AB⁺ and the distance d between the origin + # and the complex hull of these. Plugging this into 2√1-d² gives the + # diamond norm. if ( not force_solve @@ -498,55 +504,53 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, U = A * B.dag() eigs = U.eigenenergies() d = _find_poly_distance(eigs) - value = 2 * np.sqrt(1 - d**2) # plug d into formula - else: # Force the input superoperator to be a Choi matrix. - J = to_choi(A) - if B is not None: - J -= to_choi(B) - - # Watrous 2012 also points out that the diamond norm of Lambda - # is the same as the completely-bounded operator-norm (∞-norm) - # of the dual map of Lambda. We can evaluate that norm much more - # easily if Lambda is completely positive, since then the largest - # eigenvalue is the same as the largest singular value. - - if not force_solve and J.iscp: - S_dual = to_super(J.dual_chan()) - vec_eye = operator_to_vector(qeye(S_dual.dims[1][1])) - op = vector_to_operator(S_dual * vec_eye) - # The 2-norm was not implemented for sparse matrices as of the time - # of this writing. Thus, we must yet again go dense. - value = la.norm(op.full(), 2) - elif not force_solve and J.iscptp: - # diamond norm of a CPTP map is 1 (Prop 3.44 Watrous 2018) - value = 1.0 - else: - # If we're still here, we need to actually solve the problem. - - # Assume square... - dim = int(np.prod(J.dims[0][0])) - - # Load the parameters with the Choi matrix passed in. - J_dat = _data.to('csr', J.data).as_scipy() - - if not sparse: - problem, Jr, Ji = dnorm_problem(dim) - - # Load the parameters with the Choi matrix passed in. - Jr.value = sp.csr_matrix((J_dat.data.real, J_dat.indices, - J_dat.indptr), - shape=J_dat.shape).toarray() - - Ji.value = sp.csr_matrix((J_dat.data.imag, J_dat.indices, - J_dat.indptr), - shape=J_dat.shape).toarray() - else: - problem = dnorm_sparse_problem(dim, J_dat) - - problem.solve(solver=solver, verbose=verbose) - - value = problem.value - return value + return 2 * np.sqrt(1 - d**2) # plug d into formula + J = to_choi(A) + if B is not None: + J -= to_choi(B) + + # Watrous 2012 also points out that the diamond norm of Lambda + # is the same as the completely-bounded operator-norm (∞-norm) + # of the dual map of Lambda. We can evaluate that norm much more + # easily if Lambda is completely positive, since then the largest + # eigenvalue is the same as the largest singular value. + + if not force_solve and J.iscp: + S_dual = to_super(J.dual_chan()) + vec_eye = operator_to_vector(qeye(S_dual.dims[1][1])) + op = vector_to_operator(S_dual * vec_eye) + # The 2-norm was not implemented for sparse matrices as of the time + # of this writing. Thus, we must yet again go dense. + return la.norm(op.full(), 2) + if not force_solve and J.iscptp: + # diamond norm of a CPTP map is 1 (Prop 3.44 Watrous 2018) + return 1.0 + + # If we're still here, we need to actually solve the problem. + + # Assume square... + dim = int(np.prod(J.dims[0][0])) + + # Load the parameters with the Choi matrix passed in. + J_dat = _data.to('csr', J.data).as_scipy() + + if not sparse: + problem, Jr, Ji = dnorm_problem(dim) + + # Load the parameters with the Choi matrix passed in. + Jr.value = sp.csr_matrix((J_dat.data.real, J_dat.indices, + J_dat.indptr), + shape=J_dat.shape).toarray() + + Ji.value = sp.csr_matrix((J_dat.data.imag, J_dat.indices, + J_dat.indptr), + shape=J_dat.shape).toarray() + else: + problem = dnorm_sparse_problem(dim, J_dat) + + problem.solve(solver=solver, verbose=verbose) + + return problem.value def unitarity(oper): @@ -569,22 +573,27 @@ def unitarity(oper): def _find_poly_distance(eigenvals: np.ndarray) -> float: - """Function to find the distance between origin and the - convex hull of eigenvalues.""" + """ + Returns the distance between the origin and the convex hull of eigenvalues. + + The complex eigenvalues must have unit length (i.e. lie on the circle + about the origin). + """ phases = np.angle(eigenvals) - pos_max = phases.max() - neg_min = phases.min() - pos_min = np.where(phases > 0, phases, np.inf).min() - neg_max = np.where(phases <= 0, phases, -np.inf).max() + phase_max = phases.max() + phase_min = phases.min() + + if phase_min > 0: # all eigenvals have pos phase: hull is above x axis + return np.cos((phase_max - phase_min) / 2) - if neg_min > 0: # all eigenvals have positive phase, hull is above x axis - return np.cos((pos_max - pos_min) / 2) + if phase_max <= 0: # all eigenvals have neg phase: hull is below x axis + return np.cos((np.abs(phase_min) - np.abs(phase_max)) / 2) - if pos_max <= 0: # all eigenvals have negative phase, hull is below x axis - return np.cos((np.abs(neg_min) - np.abs(neg_max)) / 2) + pos_phase_min = np.where(phases > 0, phases, np.inf).min() + neg_phase_max = np.where(phases <= 0, phases, -np.inf).max() - big_angle = pos_max - neg_min - small_angle = pos_min - neg_max + big_angle = phase_max - phase_min + small_angle = pos_phase_min - neg_phase_max if big_angle >= np.pi: if small_angle <= np.pi: # hull contains the origin return 0 From 8eff22ca565c9fca8f9bd5fe6dafbb99ccfe1be7 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Wed, 8 May 2024 09:26:12 +0100 Subject: [PATCH 159/305] Added explicit unitary test --- qutip/tests/core/test_metrics.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qutip/tests/core/test_metrics.py b/qutip/tests/core/test_metrics.py index 575cafdc97..e7fbbdef8d 100644 --- a/qutip/tests/core/test_metrics.py +++ b/qutip/tests/core/test_metrics.py @@ -453,6 +453,15 @@ def test_force_solve(self, dimension, generator): == pytest.approx(dnorm(A, B, force_solve=True), abs=1e-5) ) + @pytest.mark.repeat(3) + def test_unitary_case(self, dimension): + """Check that the diamond norm is one for unitary maps.""" + A, B = rand_unitary(dimension), rand_unitary(dimension) + assert ( + dnorm(A, B) + == pytest.approx(dnorm(A, B, force_solve=True), abs=1e-5) + ) + @pytest.mark.repeat(3) def test_cptp(self, dimension, sparse): """Check that the diamond norm is one for CPTP maps.""" From 98b5e404143a6d67ead5d2ead7459f41ebf8eee9 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Wed, 8 May 2024 11:36:06 +0100 Subject: [PATCH 160/305] Fixed documentation and code logic --- doc/changes/2416.feature | 2 +- qutip/core/metrics.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/doc/changes/2416.feature b/doc/changes/2416.feature index e614e4e5ad..30dffbec0b 100644 --- a/doc/changes/2416.feature +++ b/doc/changes/2416.feature @@ -1,2 +1,2 @@ -Updated `qutip.core.metrics.dnorm` to have an efficient speedup when finding the difference of two unitaries. We use a result on page 19 of +Updated `qutip.core.metrics.dnorm` to have an efficient speedup when finding the difference of two unitaries. We use a result on page 18 of D. Aharonov, A. Kitaev, and N. Nisan, (1998). \ No newline at end of file diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index 744d61ec6e..c294a8a5b3 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -505,10 +505,16 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, eigs = U.eigenenergies() d = _find_poly_distance(eigs) return 2 * np.sqrt(1 - d**2) # plug d into formula + J = to_choi(A) - if B is not None: + + if B is not None: # If B is provided, calculate difference J -= to_choi(B) + if not force_solve and J.iscptp: + # diamond norm of a CPTP map is 1 (Prop 3.44 Watrous 2018) + return 1.0 + # Watrous 2012 also points out that the diamond norm of Lambda # is the same as the completely-bounded operator-norm (∞-norm) # of the dual map of Lambda. We can evaluate that norm much more @@ -522,9 +528,6 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, # The 2-norm was not implemented for sparse matrices as of the time # of this writing. Thus, we must yet again go dense. return la.norm(op.full(), 2) - if not force_solve and J.iscptp: - # diamond norm of a CPTP map is 1 (Prop 3.44 Watrous 2018) - return 1.0 # If we're still here, we need to actually solve the problem. From 2c454809e00405b741989c72cb2e5f4812c1a2ef Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 8 May 2024 11:55:29 -0400 Subject: [PATCH 161/305] Make compatible with numpy 2.0 --- qutip/core/_brtensor.pyx | 2 +- qutip/core/cy/coefficient.pyx | 2 +- qutip/core/data/csr.pyx | 8 ++++---- qutip/core/data/dense.pyx | 6 +++--- qutip/core/data/dia.pyx | 6 +++--- qutip/core/data/make.py | 2 +- qutip/solver/scattering.py | 2 +- qutip/tests/solver/test_correlation.py | 4 ++-- qutip/tests/solver/test_propagator.py | 2 +- qutip/tests/test_wigner.py | 12 ++++++++---- qutip/utilities.py | 2 +- 11 files changed, 26 insertions(+), 22 deletions(-) diff --git a/qutip/core/_brtensor.pyx b/qutip/core/_brtensor.pyx index 984e016ede..804e4deaf1 100644 --- a/qutip/core/_brtensor.pyx +++ b/qutip/core/_brtensor.pyx @@ -35,7 +35,7 @@ cpdef Data _br_term_data(Data A, double[:, ::1] spectrum, cdef Data S, I, AS, AST, out, C cdef type cls = type(A) - S = _data.to(cls, _data.mul(_data.Dense(spectrum, copy=False), 0.5)) + S = _data.to(cls, _data.mul(_data.Dense(spectrum, copy=None), 0.5)) I = _data.identity[cls](nrows) AS = _data.multiply(A, S) AST = _data.multiply(A, _data.transpose(S)) diff --git a/qutip/core/cy/coefficient.pyx b/qutip/core/cy/coefficient.pyx index 3df6eeca60..2f61b2c129 100644 --- a/qutip/core/cy/coefficient.pyx +++ b/qutip/core/cy/coefficient.pyx @@ -517,7 +517,7 @@ cdef class InterCoefficient(Coefficient): @classmethod def from_PPoly(cls, ppoly, **_): - return cls.restore(ppoly.x, np.array(ppoly.c, complex, copy=False)) + return cls.restore(ppoly.x, np.asarray(ppoly.c, complex, copy=None)) @classmethod def from_Bspline(cls, spline, **_): diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index 5bbce551cc..d3c99cfe5b 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -78,7 +78,7 @@ cdef class CSR(base.Data): # single flag that is set as soon as the pointers are assigned. self._deallocate = True - def __init__(self, arg=None, shape=None, bint copy=True, bint tidyup=False): + def __init__(self, arg=None, shape=None, copy=True, bint tidyup=False): # This is the Python __init__ method, so we do not care that it is not # super-fast C access. Typically Cython code will not call this, but # will use a factory method in this module or at worst, call @@ -100,9 +100,9 @@ cdef class CSR(base.Data): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 3: raise ValueError("arg must be a (data, col_index, row_index) tuple") - data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') - col_index = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') - row_index = np.array(arg[2], dtype=idxint_dtype, copy=copy, order='C') + data = np.asarray(arg[0], dtype=np.complex128, copy=copy, order='C') + col_index = np.asarray(arg[1], dtype=idxint_dtype, copy=copy, order='C') + row_index = np.asarray(arg[2], dtype=idxint_dtype, copy=copy, order='C') # This flag must be set at the same time as data, col_index and # row_index are assigned. These assignments cannot raise an exception # in user code due to the above three lines, but further code may. diff --git a/qutip/core/data/dense.pyx b/qutip/core/data/dense.pyx index 0db9034fa0..39a5592ff1 100644 --- a/qutip/core/data/dense.pyx +++ b/qutip/core/data/dense.pyx @@ -40,7 +40,7 @@ class OrderEfficiencyWarning(EfficiencyWarning): cdef class Dense(base.Data): def __init__(self, data, shape=None, copy=True): - base = np.array(data, dtype=np.complex128, order='K', copy=copy) + base = np.asarray(data, dtype=np.complex128, order='K', copy=copy) # Ensure that the array is contiguous. # Non contiguous array with copy=False would otherwise slip through if not (cnp.PyArray_IS_C_CONTIGUOUS(base) or @@ -135,8 +135,8 @@ cdef class Dense(base.Data): cdef void _fix_flags(self, object array, bint make_owner=False): cdef int enable = cnp.NPY_ARRAY_OWNDATA if make_owner else 0 cdef int disable = 0 - cdef cnp.Py_intptr_t *dims = cnp.PyArray_DIMS(array) - cdef cnp.Py_intptr_t *strides = cnp.PyArray_STRIDES(array) + cdef cnp.npy_intp *dims = cnp.PyArray_DIMS(array) + cdef cnp.npy_intp *strides = cnp.PyArray_STRIDES(array) # Not necessary when creating a new array because this will already # have been done, but needed for as_ndarray() if we have been mutated. dims[0] = self.shape[0] diff --git a/qutip/core/data/dia.pyx b/qutip/core/data/dia.pyx index 9414298402..4e2b51b46b 100644 --- a/qutip/core/data/dia.pyx +++ b/qutip/core/data/dia.pyx @@ -69,7 +69,7 @@ cdef class Dia(base.Data): def __cinit__(self, *args, **kwargs): self._deallocate = True - def __init__(self, arg=None, shape=None, bint copy=True, bint tidyup=False): + def __init__(self, arg=None, shape=None, copy=True, bint tidyup=False): cdef size_t ptr cdef base.idxint col cdef object data, offsets @@ -87,8 +87,8 @@ cdef class Dia(base.Data): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 2: raise ValueError("arg must be a (data, offsets) tuple") - data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') - offsets = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') + data = np.asarray(arg[0], dtype=np.complex128, copy=copy, order='C') + offsets = np.asarray(arg[1], dtype=idxint_dtype, copy=copy, order='C') self.num_diag = offsets.shape[0] self._max_diag = self.num_diag diff --git a/qutip/core/data/make.py b/qutip/core/data/make.py index 39c5247169..c7bfa8f681 100644 --- a/qutip/core/data/make.py +++ b/qutip/core/data/make.py @@ -119,7 +119,7 @@ def one_element_dia(shape, position, value=1.0): data = np.zeros((1, shape[1]), dtype=complex) data[0, position[1]] = value offsets = np.array([position[1]-position[0]]) - return Dia((data, offsets), copy=False, shape=shape) + return Dia((data, offsets), copy=None, shape=shape) one_element = _Dispatcher(one_element_dense, name='one_element', diff --git a/qutip/solver/scattering.py b/qutip/solver/scattering.py index 23b5450fe9..3cde6152fb 100644 --- a/qutip/solver/scattering.py +++ b/qutip/solver/scattering.py @@ -297,5 +297,5 @@ def scattering_probability(H, psi0, n_emissions, c_ops, tlist, # Iteratively integrate to obtain single value while probs.shape != (): - probs = np.trapz(probs, x=tlist) + probs = np.trapezoid(probs, x=tlist) return np.abs(probs) diff --git a/qutip/tests/solver/test_correlation.py b/qutip/tests/solver/test_correlation.py index bf96b045da..a53088be34 100644 --- a/qutip/tests/solver/test_correlation.py +++ b/qutip/tests/solver/test_correlation.py @@ -91,7 +91,7 @@ def test_spectrum_solver_equivalence_to_es(spectrum): def _trapz_2d(z, xy): """2D trapezium-method integration assuming a square grid.""" dx = xy[1] - xy[0] - return dx*dx * np.trapz(np.trapz(z, axis=0)) + return dx*dx * np.trapezoid(np.trapezoid(z, axis=0)) def _n_correlation(times, n): @@ -135,7 +135,7 @@ def _2ls_g2_0(H, c_ops): e_ops=[qutip.num(2)], args=_2ls_args).expect[0] integral_correlation = _trapz_2d(np.real(correlation), times) - integral_n_expectation = np.trapz(n_expectation, times) + integral_n_expectation = np.trapezoid(n_expectation, times) # Factor of two from negative time correlations. return 2 * integral_correlation / integral_n_expectation**2 diff --git a/qutip/tests/solver/test_propagator.py b/qutip/tests/solver/test_propagator.py index 5119fa0d6b..0e3d91afdb 100644 --- a/qutip/tests/solver/test_propagator.py +++ b/qutip/tests/solver/test_propagator.py @@ -44,7 +44,7 @@ def testPropHOTd(): Htd = [H, [H, func]] U = propagator(Htd, 1) ts = np.linspace(0, 1, 101) - U2 = (-1j * H * np.trapz(1 + func(ts), ts)).expm() + U2 = (-1j * H * np.trapezoid(1 + func(ts), ts)).expm() assert (U - U2).norm('max') < 1e-4 diff --git a/qutip/tests/test_wigner.py b/qutip/tests/test_wigner.py index 93b5e114e8..1898777c79 100644 --- a/qutip/tests/test_wigner.py +++ b/qutip/tests/test_wigner.py @@ -634,7 +634,9 @@ def test_spin_q_function_normalized(spin, pure): phi = np.linspace(-np.pi, np.pi, 256, endpoint=True) Q, THETA, _ = qutip.spin_q_function(rho, theta, phi) - norm = d / (4 * np.pi) * np.trapz(np.trapz(Q * np.sin(THETA), theta), phi) + norm = d / (4 * np.pi) * np.trapezoid( + np.trapezoid(Q * np.sin(THETA), theta), phi + ) assert_allclose(norm, 1, atol=2e-4) @@ -657,7 +659,9 @@ def test_spin_wigner_normalized(spin, pure): phi = np.linspace(-np.pi, np.pi, 512, endpoint=True) W, THETA, PHI = qutip.spin_wigner(rho, theta, phi) - norm = np.trapz(np.trapz(W * np.sin(THETA) * np.sqrt(d / (4*np.pi)), theta), phi) + norm = np.trapezoid( + np.trapezoid(W * np.sin(THETA) * np.sqrt(d / (4*np.pi)), theta), phi + ) assert_almost_equal(norm, 1, decimal=4) @pytest.mark.parametrize(['spin'], [ @@ -684,6 +688,6 @@ def test_spin_wigner_overlap(spin, pure, n=5): state_overlap = (test_state*rho).tr().real W_state, _, _ = qutip.spin_wigner(test_state, theta, phi) - W_overlap = np.trapz( - np.trapz(W_state * W * np.sin(THETA), theta), phi).real + W_overlap = np.trapezoid( + np.trapezoid(W_state * W * np.sin(THETA), theta), phi).real assert_almost_equal(W_overlap, state_overlap, decimal=4) diff --git a/qutip/utilities.py b/qutip/utilities.py index 2032b4be30..806b874362 100644 --- a/qutip/utilities.py +++ b/qutip/utilities.py @@ -108,7 +108,7 @@ def clebsch(j1, j2, j3, m1, m2, m3): C = np.sqrt((2.0 * j3 + 1.0)*_to_long(c_factor)) s_factors = np.zeros(((vmax + 1 - vmin), (int(j1 + j2 + j3))), np.int32) - sign = (-1) ** (vmin + j2 + m2) + sign = int((-1) ** (vmin + j2 + m2)) for i,v in enumerate(range(vmin, vmax + 1)): factor = s_factors[i,:] _factorial_prod(j2 + j3 + m1 - v, factor) From 6ddf3cf6aeed332d329db6965fa2edde88725079 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Wed, 8 May 2024 17:49:46 +0100 Subject: [PATCH 162/305] Fixed tests --- qutip/tests/core/test_metrics.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/qutip/tests/core/test_metrics.py b/qutip/tests/core/test_metrics.py index e7fbbdef8d..def3e6b1a9 100644 --- a/qutip/tests/core/test_metrics.py +++ b/qutip/tests/core/test_metrics.py @@ -439,31 +439,25 @@ def test_qubit_triangle(self, dimension): assert dnorm(A + B) <= dnorm(A) + dnorm(B) + 1e-7 @pytest.mark.repeat(3) - @pytest.mark.parametrize("generator", [ - pytest.param(rand_super_bcsz, id="super"), - pytest.param(rand_unitary, id="unitary"), - ]) - def test_force_solve(self, dimension, generator): - """ - Metrics: checks that special cases for dnorm agree with SDP solutions. - """ - A, B = generator(dimension), generator(dimension) + def test_unitary_case(self, dimension): + """Check that the diamond norm is one for unitary maps.""" + A, B = rand_unitary(dimension), rand_unitary(dimension) assert ( - dnorm(A, B, force_solve=False) + dnorm(A, B) == pytest.approx(dnorm(A, B, force_solve=True), abs=1e-5) ) @pytest.mark.repeat(3) - def test_unitary_case(self, dimension): + def test_cp_case(self, dimension): """Check that the diamond norm is one for unitary maps.""" - A, B = rand_unitary(dimension), rand_unitary(dimension) + A = rand_super_bcsz(dimension, enforce_tp=False) assert ( - dnorm(A, B) - == pytest.approx(dnorm(A, B, force_solve=True), abs=1e-5) + dnorm(A) + == pytest.approx(dnorm(A, force_solve=True), abs=1e-5) ) @pytest.mark.repeat(3) - def test_cptp(self, dimension, sparse): + def test_cptp_case(self, dimension, sparse): """Check that the diamond norm is one for CPTP maps.""" A = rand_super_bcsz(dimension) assert A.iscptp From 210e140f954b289e6d31d14ba742efec1f3b41a8 Mon Sep 17 00:00:00 2001 From: owenagnel Date: Wed, 8 May 2024 17:50:39 +0100 Subject: [PATCH 163/305] Fixed typo in comment --- qutip/core/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/metrics.py b/qutip/core/metrics.py index c294a8a5b3..432f693f00 100644 --- a/qutip/core/metrics.py +++ b/qutip/core/metrics.py @@ -500,7 +500,7 @@ def dnorm(A, B=None, solver="CVXOPT", verbose=False, force_solve=False, and A.isunitary and B is not None and B.isunitary - ): # Special optimisation fo a difference of unitaries. + ): # Special optimisation for a difference of unitaries. U = A * B.dag() eigs = U.eigenenergies() d = _find_poly_distance(eigs) From 6bbfcba4fec4974dddb46082de929b865022ee1e Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 8 May 2024 13:49:18 -0400 Subject: [PATCH 164/305] Adapt for numpy 2 and add it to pytest matrix --- .github/workflows/tests.yml | 23 +++++++++++++++++++++++ qutip/core/_brtensor.pyx | 2 +- qutip/core/cy/coefficient.pyx | 2 +- qutip/core/data/csr.pyx | 6 +++--- qutip/core/data/dense.pyx | 2 +- qutip/core/data/dia.pyx | 4 ++-- qutip/core/data/make.py | 6 +++++- qutip/solver/scattering.py | 3 ++- qutip/tests/solver/test_correlation.py | 5 +++-- qutip/tests/solver/test_propagator.py | 3 ++- qutip/tests/test_wigner.py | 13 +++++++------ 11 files changed, 50 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7a6c082d46..edd40f9fe7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -50,6 +50,22 @@ jobs: oldcython: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" + # Test with the version 2 of numpy comming soon. + - case-name: numpy2 + os: ubuntu-latest + python-version: "3.12" + scipy-requirement: "" + numpy-requirement: "==2.0.0rc1" + + # Binaries compiled with numpy 2 should be compatible when using + # numpy 1.X at runtime. + - case-name: numpy2_to_1 + os: ubuntu-latest + python-version: "3.9" + scipy-requirement: "" + numpy-requirement: "==2.0.0rc1" + roll_back_numpy: 1 + # Python 3.10, no mkl, scipy 1.9, numpy 1.23 # Scipy 1.9 did not support cython 3.0 yet. # cython#17234 @@ -172,6 +188,13 @@ jobs: python -m pip uninstall cython -y fi + if [[ "${{ matrix.roll_back_numpy }}" ]]; then + # Binary compiled with numpy 2.X should be compatible with numpy 1.X + python -m pip install "numpy<=1.23" + fi + + + - name: Package information run: | conda list diff --git a/qutip/core/_brtensor.pyx b/qutip/core/_brtensor.pyx index 804e4deaf1..344562dc68 100644 --- a/qutip/core/_brtensor.pyx +++ b/qutip/core/_brtensor.pyx @@ -35,7 +35,7 @@ cpdef Data _br_term_data(Data A, double[:, ::1] spectrum, cdef Data S, I, AS, AST, out, C cdef type cls = type(A) - S = _data.to(cls, _data.mul(_data.Dense(spectrum, copy=None), 0.5)) + S = _data.to(cls, _data.mul(_data.Dense(spectrum), 0.5)) I = _data.identity[cls](nrows) AS = _data.multiply(A, S) AST = _data.multiply(A, _data.transpose(S)) diff --git a/qutip/core/cy/coefficient.pyx b/qutip/core/cy/coefficient.pyx index 2f61b2c129..a61ec16181 100644 --- a/qutip/core/cy/coefficient.pyx +++ b/qutip/core/cy/coefficient.pyx @@ -517,7 +517,7 @@ cdef class InterCoefficient(Coefficient): @classmethod def from_PPoly(cls, ppoly, **_): - return cls.restore(ppoly.x, np.asarray(ppoly.c, complex, copy=None)) + return cls.restore(ppoly.x, np.asarray(ppoly.c, complex)) @classmethod def from_Bspline(cls, spline, **_): diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index d3c99cfe5b..385c0ebe10 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -100,9 +100,9 @@ cdef class CSR(base.Data): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 3: raise ValueError("arg must be a (data, col_index, row_index) tuple") - data = np.asarray(arg[0], dtype=np.complex128, copy=copy, order='C') - col_index = np.asarray(arg[1], dtype=idxint_dtype, copy=copy, order='C') - row_index = np.asarray(arg[2], dtype=idxint_dtype, copy=copy, order='C') + data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') + col_index = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') + row_index = np.array(arg[2], dtype=idxint_dtype, copy=copy, order='C') # This flag must be set at the same time as data, col_index and # row_index are assigned. These assignments cannot raise an exception # in user code due to the above three lines, but further code may. diff --git a/qutip/core/data/dense.pyx b/qutip/core/data/dense.pyx index 39a5592ff1..723ad054c9 100644 --- a/qutip/core/data/dense.pyx +++ b/qutip/core/data/dense.pyx @@ -40,7 +40,7 @@ class OrderEfficiencyWarning(EfficiencyWarning): cdef class Dense(base.Data): def __init__(self, data, shape=None, copy=True): - base = np.asarray(data, dtype=np.complex128, order='K', copy=copy) + base = np.array(data, dtype=np.complex128, order='K', copy=copy) # Ensure that the array is contiguous. # Non contiguous array with copy=False would otherwise slip through if not (cnp.PyArray_IS_C_CONTIGUOUS(base) or diff --git a/qutip/core/data/dia.pyx b/qutip/core/data/dia.pyx index 4e2b51b46b..10c51bcf54 100644 --- a/qutip/core/data/dia.pyx +++ b/qutip/core/data/dia.pyx @@ -87,8 +87,8 @@ cdef class Dia(base.Data): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 2: raise ValueError("arg must be a (data, offsets) tuple") - data = np.asarray(arg[0], dtype=np.complex128, copy=copy, order='C') - offsets = np.asarray(arg[1], dtype=idxint_dtype, copy=copy, order='C') + data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') + offsets = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') self.num_diag = offsets.shape[0] self._max_diag = self.num_diag diff --git a/qutip/core/data/make.py b/qutip/core/data/make.py index c7bfa8f681..c36edd304a 100644 --- a/qutip/core/data/make.py +++ b/qutip/core/data/make.py @@ -119,7 +119,11 @@ def one_element_dia(shape, position, value=1.0): data = np.zeros((1, shape[1]), dtype=complex) data[0, position[1]] = value offsets = np.array([position[1]-position[0]]) - return Dia((data, offsets), copy=None, shape=shape) + if np.lib.NumpyVersion(np.__version__) >= '2.0.0b1': + copy_if_needed = None + else: + copy_if_needed = False + return Dia((data, offsets), copy=copy_if_needed, shape=shape) one_element = _Dispatcher(one_element_dense, name='one_element', diff --git a/qutip/solver/scattering.py b/qutip/solver/scattering.py index 3cde6152fb..80a8bd8855 100644 --- a/qutip/solver/scattering.py +++ b/qutip/solver/scattering.py @@ -11,6 +11,7 @@ # Contact: benbartlett@stanford.edu import numpy as np +from scipy.integrate import trapezoid from itertools import product, combinations_with_replacement from ..core import basis, tensor, zero_ket, Qobj, QobjEvo from .propagator import propagator, Propagator @@ -297,5 +298,5 @@ def scattering_probability(H, psi0, n_emissions, c_ops, tlist, # Iteratively integrate to obtain single value while probs.shape != (): - probs = np.trapezoid(probs, x=tlist) + probs = trapezoid(probs, x=tlist) return np.abs(probs) diff --git a/qutip/tests/solver/test_correlation.py b/qutip/tests/solver/test_correlation.py index a53088be34..1a58247e88 100644 --- a/qutip/tests/solver/test_correlation.py +++ b/qutip/tests/solver/test_correlation.py @@ -2,6 +2,7 @@ import functools from itertools import product import numpy as np +from scipy.integrate import trapezoid import qutip pytestmark = [pytest.mark.usefixtures("in_temporary_directory")] @@ -91,7 +92,7 @@ def test_spectrum_solver_equivalence_to_es(spectrum): def _trapz_2d(z, xy): """2D trapezium-method integration assuming a square grid.""" dx = xy[1] - xy[0] - return dx*dx * np.trapezoid(np.trapezoid(z, axis=0)) + return dx*dx * trapezoid(trapezoid(z, axis=0)) def _n_correlation(times, n): @@ -135,7 +136,7 @@ def _2ls_g2_0(H, c_ops): e_ops=[qutip.num(2)], args=_2ls_args).expect[0] integral_correlation = _trapz_2d(np.real(correlation), times) - integral_n_expectation = np.trapezoid(n_expectation, times) + integral_n_expectation = trapezoid(n_expectation, times) # Factor of two from negative time correlations. return 2 * integral_correlation / integral_n_expectation**2 diff --git a/qutip/tests/solver/test_propagator.py b/qutip/tests/solver/test_propagator.py index 0e3d91afdb..ffb335cbe3 100644 --- a/qutip/tests/solver/test_propagator.py +++ b/qutip/tests/solver/test_propagator.py @@ -1,4 +1,5 @@ import numpy as np +from scipy.integrate import trapezoid from qutip import (destroy, propagator, Propagator, propagator_steadystate, steadystate, tensor, qeye, basis, QobjEvo, sesolve, liouvillian) @@ -44,7 +45,7 @@ def testPropHOTd(): Htd = [H, [H, func]] U = propagator(Htd, 1) ts = np.linspace(0, 1, 101) - U2 = (-1j * H * np.trapezoid(1 + func(ts), ts)).expm() + U2 = (-1j * H * trapezoid(1 + func(ts), ts)).expm() assert (U - U2).norm('max') < 1e-4 diff --git a/qutip/tests/test_wigner.py b/qutip/tests/test_wigner.py index 1898777c79..d958183d6c 100644 --- a/qutip/tests/test_wigner.py +++ b/qutip/tests/test_wigner.py @@ -1,5 +1,6 @@ import pytest import numpy as np +from scipy.integrate import trapezoid import itertools from scipy.special import laguerre from numpy.random import rand @@ -634,8 +635,8 @@ def test_spin_q_function_normalized(spin, pure): phi = np.linspace(-np.pi, np.pi, 256, endpoint=True) Q, THETA, _ = qutip.spin_q_function(rho, theta, phi) - norm = d / (4 * np.pi) * np.trapezoid( - np.trapezoid(Q * np.sin(THETA), theta), phi + norm = d / (4 * np.pi) * trapezoid( + trapezoid(Q * np.sin(THETA), theta), phi ) assert_allclose(norm, 1, atol=2e-4) @@ -659,8 +660,8 @@ def test_spin_wigner_normalized(spin, pure): phi = np.linspace(-np.pi, np.pi, 512, endpoint=True) W, THETA, PHI = qutip.spin_wigner(rho, theta, phi) - norm = np.trapezoid( - np.trapezoid(W * np.sin(THETA) * np.sqrt(d / (4*np.pi)), theta), phi + norm = trapezoid( + trapezoid(W * np.sin(THETA) * np.sqrt(d / (4*np.pi)), theta), phi ) assert_almost_equal(norm, 1, decimal=4) @@ -688,6 +689,6 @@ def test_spin_wigner_overlap(spin, pure, n=5): state_overlap = (test_state*rho).tr().real W_state, _, _ = qutip.spin_wigner(test_state, theta, phi) - W_overlap = np.trapezoid( - np.trapezoid(W_state * W * np.sin(THETA), theta), phi).real + W_overlap = trapezoid( + trapezoid(W_state * W * np.sin(THETA), theta), phi).real assert_almost_equal(W_overlap, state_overlap, decimal=4) From 69533ba33df71c702c832d9604d75d94b98b7257 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 8 May 2024 14:01:35 -0400 Subject: [PATCH 165/305] install from pypi --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edd40f9fe7..806c9b3483 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,8 +56,9 @@ jobs: python-version: "3.12" scipy-requirement: "" numpy-requirement: "==2.0.0rc1" + pypi: 1 - # Binaries compiled with numpy 2 should be compatible when using + # Binaries compiled with numpy 2 should be compatible when using # numpy 1.X at runtime. - case-name: numpy2_to_1 os: ubuntu-latest @@ -65,6 +66,7 @@ jobs: scipy-requirement: "" numpy-requirement: "==2.0.0rc1" roll_back_numpy: 1 + pypi: 1 # numpy 2 not yet available on conda # Python 3.10, no mkl, scipy 1.9, numpy 1.23 # Scipy 1.9 did not support cython 3.0 yet. @@ -160,7 +162,9 @@ jobs: python -m pip install "coverage${{ matrix.coverage-requirement }}" chardet python -m pip install pytest-cov coveralls pytest-fail-slow - if [[ -z "${{ matrix.nomkl }}" ]]; then + if [[ "${{ matrix.pypi }}" ]]; then + pip install "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" + elif [[ -z "${{ matrix.nomkl }}" ]]; then conda install blas=*=mkl "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" elif [[ "${{ matrix.os }}" =~ ^windows.*$ ]]; then # Conda doesn't supply forced nomkl builds on Windows, so we rely on From bf9a07ef31dc040c2fe891c8aff9559c4bfa7011 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 8 May 2024 14:22:40 -0400 Subject: [PATCH 166/305] test roll back with oldest supported numpy --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 806c9b3483..1080dd1f0a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -194,7 +194,7 @@ jobs: if [[ "${{ matrix.roll_back_numpy }}" ]]; then # Binary compiled with numpy 2.X should be compatible with numpy 1.X - python -m pip install "numpy<=1.23" + python -m pip install "numpy<1.23" fi From 8f3bbe0b876677747e221de4d15c21f3abe978f7 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 8 May 2024 14:47:01 -0400 Subject: [PATCH 167/305] Add towncrier --- doc/changes/2421.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2421.misc diff --git a/doc/changes/2421.misc b/doc/changes/2421.misc new file mode 100644 index 0000000000..0ebb145cf5 --- /dev/null +++ b/doc/changes/2421.misc @@ -0,0 +1 @@ +Add support for numpy 2 From df553f24a9f4150f93b7e5643a299d9da11ce666 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 9 May 2024 18:20:30 +0900 Subject: [PATCH 168/305] Avoid error with stochastic solver and no stochastic collapse operators --- qutip/solver/stochastic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 67e130efcd..73206b4586 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -83,6 +83,11 @@ def measurement(self): """ if not self.options["store_measurement"]: return None + elif len(self.m_ops) == 0: + if self.heterodyne: + return np.empty(shape=(0, 2, len(self.times) - 1)) + else: + return np.empty(shape=(0, len(self.times) - 1)) elif self.options["store_measurement"] == "start": m_expect = np.array(self.m_expect)[:, :-1] elif self.options["store_measurement"] == "middle": From ed3329417a91421e46705f2d3da47e7138c39f00 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 9 May 2024 18:24:59 +0900 Subject: [PATCH 169/305] Added stochastic tests without sc_ops --- qutip/tests/solver/test_stochastic.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index eaf2f9c015..ca36592b55 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -15,6 +15,10 @@ def _make_system(N, system): gamma = 0.25 a = destroy(N) + if system == "no sc_ops": + H = a.dag() * a + sc_ops = [] + if system == "simple": H = a.dag() * a sc_ops = [np.sqrt(gamma) * a] @@ -39,7 +43,7 @@ def _make_system(N, system): @pytest.mark.parametrize("system", [ - "simple", "2 c_ops", "H td", "complex", "c_ops td", + "no sc_ops", "simple", "2 c_ops", "H td", "complex", "c_ops td", ]) @pytest.mark.parametrize("heterodyne", [True, False]) def test_smesolve(heterodyne, system): @@ -138,7 +142,7 @@ def test_smesolve_methods(method, heterodyne): @pytest.mark.parametrize("system", [ - "simple", "2 c_ops", "H td", "complex", "c_ops td", + "no sc_ops", "simple", "2 c_ops", "H td", "complex", "c_ops td", ]) @pytest.mark.parametrize("heterodyne", [True, False]) def test_ssesolve(heterodyne, system): From 9e628b30e0622c3a5bf42471492b0eee4a53a8a5 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 9 May 2024 18:32:47 +0900 Subject: [PATCH 170/305] More stochastic tests without sc_ops --- qutip/tests/solver/test_stochastic.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index ca36592b55..9567d0259d 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -77,13 +77,15 @@ def test_smesolve(heterodyne, system): ) +@pytest.mark.parametrize("system", [ + "no sc_ops", "simple" +]) @pytest.mark.parametrize("heterodyne", [True, False]) @pytest.mark.parametrize("method", SMESolver.avail_integrators().keys()) -def test_smesolve_methods(method, heterodyne): +def test_smesolve_methods(method, heterodyne, system): tol = 0.05 N = 4 ntraj = 20 - system = "simple" H, sc_ops = _make_system(N, system) c_ops = [destroy(N)] @@ -178,14 +180,16 @@ def test_ssesolve(heterodyne, system): assert res.dW is None +@pytest.mark.parametrize("system", [ + "no sc_ops", "simple" +]) @pytest.mark.parametrize("heterodyne", [True, False]) @pytest.mark.parametrize("method", SSESolver.avail_integrators().keys()) -def test_ssesolve_method(method, heterodyne): +def test_ssesolve_method(method, heterodyne, system): "Stochastic: smesolve: homodyne, time-dependent H" tol = 0.1 N = 4 ntraj = 20 - system = "simple" H, sc_ops = _make_system(N, system) psi0 = coherent(N, 0.5) From f6ec6fec3d4ce1a9e33480fd3d37add4035f58da Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 9 May 2024 12:53:50 -0400 Subject: [PATCH 171/305] Ensure feedback work for all traj --- qutip/core/cy/qobjevo.pyx | 11 ++++++++++- qutip/tests/solver/test_mcsolve.py | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index d4037b72c1..fd48edfe83 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -482,7 +482,16 @@ cdef class QobjEvo: f"Desired feedback {key} is not available for the {solver}." ) new_args[key] = solvers_feeds[feed] - self.arguments(**new_args) + # self.arguments(new_args) + + if new_args: + cache = [] + self.elements = [ + element.replace_arguments(new_args, cache=cache) + for element in self.elements + ] + + def _update_feedback(QobjEvo self, QobjEvo other=None): """ diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 8e250ba743..61eb8a5bd8 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -474,6 +474,11 @@ def test_MCSolver_stepping(): assert state.isket +def _coeff_collapse(t, A): + if t == 0: + assert len(A) == 0 + return (len(A) < 3) * 1.0 + @pytest.mark.parametrize(["func", "kind"], [ pytest.param( lambda t, A: A-4, @@ -482,7 +487,7 @@ def test_MCSolver_stepping(): id="expect" ), pytest.param( - lambda t, A: (len(A) < 3) * 1.0, + _coeff_collapse, lambda: qutip.MCSolver.CollapseFeedback(), id="collapse" ), From 6abcd548b56df20c0ce1b1e4723462cf5d7ef3d2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 9 May 2024 13:05:10 -0400 Subject: [PATCH 172/305] Improve mcsolve feedback test --- qutip/tests/solver/test_mcsolve.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 61eb8a5bd8..436eec34f7 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -476,14 +476,18 @@ def test_MCSolver_stepping(): def _coeff_collapse(t, A): if t == 0: + # New trajectory, was collapse list reset? assert len(A) == 0 + if t > 2.75: + # End of the trajectory, was collapse list was filled? + assert len(A) != 0 return (len(A) < 3) * 1.0 + @pytest.mark.parametrize(["func", "kind"], [ pytest.param( lambda t, A: A-4, lambda: qutip.MCSolver.ExpectFeedback(qutip.num(10)), - # 7.+0j, id="expect" ), pytest.param( @@ -500,9 +504,9 @@ def test_feedback(func, kind): solver = qutip.MCSolver( H, c_ops=[qutip.QobjEvo([a, func], args={"A": kind()})], - options={"map": "serial"} + options={"map": "serial", "max_step": 0.2} ) result = solver.run( - psi0,np.linspace(0, 3, 31), e_ops=[qutip.num(10)], ntraj=10 + psi0, np.linspace(0, 3, 31), e_ops=[qutip.num(10)], ntraj=10 ) assert np.all(result.expect[0] > 4. - tol) From e0fe69d1ab1ca6f0ee3dd80813ef1cd226b9b0c5 Mon Sep 17 00:00:00 2001 From: Simon Cross Date: Thu, 9 May 2024 23:03:05 +0200 Subject: [PATCH 173/305] Removed commented out old code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Eric Giguère --- qutip/core/cy/qobjevo.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index fd48edfe83..c943f76b35 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -482,7 +482,6 @@ cdef class QobjEvo: f"Desired feedback {key} is not available for the {solver}." ) new_args[key] = solvers_feeds[feed] - # self.arguments(new_args) if new_args: cache = [] From 66e263b07ff11844801561dcfab3f66f7e6abe8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Thu, 9 May 2024 17:30:20 -0400 Subject: [PATCH 174/305] Update qutip/core/cy/qobjevo.pyx --- qutip/core/cy/qobjevo.pyx | 2 -- 1 file changed, 2 deletions(-) diff --git a/qutip/core/cy/qobjevo.pyx b/qutip/core/cy/qobjevo.pyx index c943f76b35..a65a99ebad 100644 --- a/qutip/core/cy/qobjevo.pyx +++ b/qutip/core/cy/qobjevo.pyx @@ -490,8 +490,6 @@ cdef class QobjEvo: for element in self.elements ] - - def _update_feedback(QobjEvo self, QobjEvo other=None): """ Merge feedback from ``op`` into self. From aab660e33b75e5eaf6059c0b958e5d5bca9496f1 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 9 May 2024 17:36:30 -0400 Subject: [PATCH 175/305] Add isequal dispatched function --- qutip/core/data/properties.pyx | 164 +++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) diff --git a/qutip/core/data/properties.pyx b/qutip/core/data/properties.pyx index 1a6e25d35b..7a97dbc020 100644 --- a/qutip/core/data/properties.pyx +++ b/qutip/core/data/properties.pyx @@ -9,6 +9,7 @@ from qutip.settings import settings from qutip.core.data.base cimport idxint from qutip.core.data cimport csr, dense, CSR, Dense, Dia from qutip.core.data.adjoint cimport transpose_csr +import numpy as np cdef extern from *: # Not defined in cpython.mem for some reason, but is in pymem.h. @@ -36,6 +37,29 @@ cdef inline bint _feq_zero(double complex a, double tol) nogil: cdef inline double _abssq(double complex x) nogil: return x.real*x.real + x.imag*x.imag +cdef inline bint _feq(double complex a, double complex b, double atol, double rtol) nogil: + """ + Follow numpy.allclose tolerance equation: + |a - b| <= (atol + rtol * |b|) + Avoid slow sqrt. + """ + cdef double diff = (a.real - b.real)**2 + (a.imag - b.imag)**2 - atol * atol + if diff_sq <= 0: + # Early exit if under atol. + # |a - b|**2 <= atol**2 + return True + cdef double normb_sq = a.real * a.real + b.imag * b.imag + if normb_sq == 0. or rtol == 0.: + # No rtol term, the previous computation was final. + return False + diff_sq -= rtol * rtol * normb_sq + if diff_sq <= 0: + # Early exit if under atol + rtol without cross term. + # |a - b|**2 <= atol**2 + (rtol * |b|)**2 + return True + # Full computation + return diff_sq**2 <= 4 * atol * rtol * normb_sq + cdef bint _isherm_csr_full(CSR matrix, double tol) except 2: """ @@ -300,6 +324,102 @@ cpdef bint iszero_dense(Dense matrix, double tol=-1) nogil: return True +cpdef bint isequal_dia(Dia A, Dia B, double atol=-1, double rtol=-1) nogil: + if A.shape[0] != B.shape[0] or A.shape[1] != B.shape[1]: + return False + if atol < 0 or rtol < 0: + # Claim the gil only once + with gil: + if atol < 0: + atol = settings.core["atol"] + if rtol < 0: + rtol = settings.core["rtol"] + + cdef idxint diag_a=0, diag_b=0 + cdef double complex *ptr_a + cdef double complex *ptr_b + cdef bint sorted=True + cdef int length, size=A.shape[1] + + ptr_a = A.data + ptr_b = B.data + + while diag_a < A.num_diag and diag_b < B.num_diag: + if A.offsets[diag_a] == B.offsets[diag_b]: + for i in range(size): + if not _feq(ptr_a[i], ptr_b[i], atol, rtol): + return False + ptr_a += size + diag_a += 1 + ptr_b += size + diag_b += 1 + elif A.offsets[diag_a] <= B.offsets[diag_b]: + for i in range(size): + if not _feq(ptr_a[i], 0., atol, rtol): + return False + ptr_a += size + diag_a += 1 + else: + for i in range(size): + if not _feq(0., ptr_b[i], atol, rtol): + return False + ptr_b += size + diag_b += 1 + return True + + +cpdef bint isequal_dense(Dense A, Dense B, double atol=-1, double rtol=-1): + if atol < 0: + atol = settings.core["atol"] + if rtol < 0: + rtol = settings.core["rtol"] + return np.allclose(A.as_ndarray(), B.as_ndarray(), rtol, atol) + + +cpdef bint isequal_csr(CSR A, CSR B, double atol=-1, double rtol=-1) nogil: + if A.shape[0] != B.shape[0] or A.shape[1] != B.shape[1]: + return False + if atol < 0 or rtol < 0: + # Claim the gil only once + with gil: + if atol < 0: + atol = settings.core["atol"] + if rtol < 0: + rtol = settings.core["rtol"] + + cdef idxint row, ptr_a, ptr_b, ptr_a_max, ptr_b_max, col_a, col_b + + ptr_a_max = ptr_b_max = 0 + for row in range(a.shape[0]): + ptr_a = ptr_a_max + ptr_a_max = a.row_index[row + 1] + ptr_b = ptr_b_max + ptr_b_max = b.row_index[row + 1] + col_a = a.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_b = b.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + while ptr_a < ptr_a_max or ptr_b < ptr_b_max: + if col_a == col_b: + if not _feq(A.data[ptr_a], B.data[ptr_b], atol, rtol): + return False + ptr_a += 1 + ptr_b += 1 + col_a = a.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_b = b.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + elif col_a < col_b: + if not _feq(A.data[ptr_a], 0., atol, rtol): + return False + ptr_a += 1 + col_a = a.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + else: + if not _feq(0., B.data[ptr_b], atol, rtol): + return False + acc_scatter(acc, b.data[ptr_b], col_b) + ptr_b += 1 + col_b = b.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + + return True + + from .dispatch import Dispatcher as _Dispatcher import inspect as _inspect @@ -397,4 +517,48 @@ iszero.add_specialisations([ (Dense, iszero_dense), ], _defer=True) +isequal = _Dispatcher( + _inspect.Signature([ + _inspect.Parameter('A', _inspect.Parameter.POSITIONAL_ONLY), + _inspect.Parameter('B', _inspect.Parameter.POSITIONAL_ONLY), + _inspect.Parameter('atol', _inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=-1), + _inspect.Parameter('rtol', _inspect.Parameter.POSITIONAL_OR_KEYWORD, + default=-1), + ]), + name='isequal', + module=__name__, + inputs=('A', 'B',), + out=False, +) +isequal.__doc__ =\ + """ + Test if two matrices are equal up to absolute and relative tolerance: + + |A - B| <= atol + rtol * |b| + + Similar to ``numpy.allclose``. + + Parameters + ---------- + A, B : Data + Matrices to compare. + atol : real, optional + The absolute tolerance to use. If not given, or + less than 0, use the core setting `atol`. + rtol : real, optional + The relative tolerance to use. If not given, or + less than 0, use the core setting `atol`. + + Returns + ------- + bool + Whether the matrix are equal. + """ +iszero.add_specialisations([ + (CSR, isequal_csr), + (Dia, isequal_dia), + (Dense, isequal_dense), +], _defer=True) + del _inspect, _Dispatcher From 474bc354e04670fadbd5ea2cacac4128d7fa4a9f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 10 May 2024 17:03:05 -0400 Subject: [PATCH 176/305] Add test + fixes --- qutip/core/data/properties.pyx | 55 ++++++++++---------- qutip/tests/core/data/conftest.py | 2 +- qutip/tests/core/data/test_properties.py | 66 ++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 27 deletions(-) diff --git a/qutip/core/data/properties.pyx b/qutip/core/data/properties.pyx index 7a97dbc020..b5f36f9414 100644 --- a/qutip/core/data/properties.pyx +++ b/qutip/core/data/properties.pyx @@ -19,6 +19,7 @@ __all__ = [ 'isherm', 'isherm_csr', 'isherm_dense', 'isherm_dia', 'isdiag', 'isdiag_csr', 'isdiag_dense', 'isdiag_dia', 'iszero', 'iszero_csr', 'iszero_dense', 'iszero_dia', + 'isequal', 'isequal_csr', 'isequal_dense', 'isequal_dia', ] cdef inline bint _conj_feq(double complex a, double complex b, double tol) nogil: @@ -44,21 +45,22 @@ cdef inline bint _feq(double complex a, double complex b, double atol, double rt Avoid slow sqrt. """ cdef double diff = (a.real - b.real)**2 + (a.imag - b.imag)**2 - atol * atol - if diff_sq <= 0: + if diff <= 0: # Early exit if under atol. # |a - b|**2 <= atol**2 return True - cdef double normb_sq = a.real * a.real + b.imag * b.imag + cdef double normb_sq = b.real * b.real + b.imag * b.imag if normb_sq == 0. or rtol == 0.: # No rtol term, the previous computation was final. return False - diff_sq -= rtol * rtol * normb_sq - if diff_sq <= 0: + diff -= rtol * rtol * normb_sq + if diff <= 0: # Early exit if under atol + rtol without cross term. # |a - b|**2 <= atol**2 + (rtol * |b|)**2 return True # Full computation - return diff_sq**2 <= 4 * atol * rtol * normb_sq + # (|a - b|**2 - atol**2 * (rtol * |b|)**2)**2 <= (2* atol * rtol * |b|)**2 + return diff**2 <= 4 * atol * atol * rtol * rtol * normb_sq cdef bint _isherm_csr_full(CSR matrix, double tol) except 2: @@ -338,8 +340,7 @@ cpdef bint isequal_dia(Dia A, Dia B, double atol=-1, double rtol=-1) nogil: cdef idxint diag_a=0, diag_b=0 cdef double complex *ptr_a cdef double complex *ptr_b - cdef bint sorted=True - cdef int length, size=A.shape[1] + cdef idxint size=A.shape[1] ptr_a = A.data ptr_b = B.data @@ -369,11 +370,13 @@ cpdef bint isequal_dia(Dia A, Dia B, double atol=-1, double rtol=-1) nogil: cpdef bint isequal_dense(Dense A, Dense B, double atol=-1, double rtol=-1): - if atol < 0: - atol = settings.core["atol"] - if rtol < 0: - rtol = settings.core["rtol"] - return np.allclose(A.as_ndarray(), B.as_ndarray(), rtol, atol) + if A.shape[0] != B.shape[0] or A.shape[1] != B.shape[1]: + return False + if atol < 0: + atol = settings.core["atol"] + if rtol < 0: + rtol = settings.core["rtol"] + return np.allclose(A.as_ndarray(), B.as_ndarray(), rtol, atol) cpdef bint isequal_csr(CSR A, CSR B, double atol=-1, double rtol=-1) nogil: @@ -388,34 +391,34 @@ cpdef bint isequal_csr(CSR A, CSR B, double atol=-1, double rtol=-1) nogil: rtol = settings.core["rtol"] cdef idxint row, ptr_a, ptr_b, ptr_a_max, ptr_b_max, col_a, col_b + cdef idxint ncols = A.shape[1] ptr_a_max = ptr_b_max = 0 - for row in range(a.shape[0]): + for row in range(A.shape[0]): ptr_a = ptr_a_max - ptr_a_max = a.row_index[row + 1] + ptr_a_max = A.row_index[row + 1] ptr_b = ptr_b_max - ptr_b_max = b.row_index[row + 1] - col_a = a.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 - col_b = b.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + ptr_b_max = B.row_index[row + 1] + col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 while ptr_a < ptr_a_max or ptr_b < ptr_b_max: if col_a == col_b: if not _feq(A.data[ptr_a], B.data[ptr_b], atol, rtol): return False ptr_a += 1 ptr_b += 1 - col_a = a.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 - col_b = b.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 elif col_a < col_b: if not _feq(A.data[ptr_a], 0., atol, rtol): return False ptr_a += 1 - col_a = a.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 else: if not _feq(0., B.data[ptr_b], atol, rtol): return False - acc_scatter(acc, b.data[ptr_b], col_b) ptr_b += 1 - col_b = b.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 return True @@ -555,10 +558,10 @@ isequal.__doc__ =\ bool Whether the matrix are equal. """ -iszero.add_specialisations([ - (CSR, isequal_csr), - (Dia, isequal_dia), - (Dense, isequal_dense), +isequal.add_specialisations([ + (CSR, CSR, isequal_csr), + (Dia, Dia, isequal_dia), + (Dense, Dense, isequal_dense), ], _defer=True) del _inspect, _Dispatcher diff --git a/qutip/tests/core/data/conftest.py b/qutip/tests/core/data/conftest.py index b4348eaa46..6b8b568e73 100644 --- a/qutip/tests/core/data/conftest.py +++ b/qutip/tests/core/data/conftest.py @@ -67,7 +67,7 @@ def random_scipy_csr(shape, density, sorted_): cols = np.random.choice(np.arange(shape[1]), nnz) sci = scipy.sparse.coo_matrix((data, (rows, cols)), shape=shape).tocsr() if not sorted_: - shuffle_indices_scipy_csr(sci) + sci = shuffle_indices_scipy_csr(sci) return sci diff --git a/qutip/tests/core/data/test_properties.py b/qutip/tests/core/data/test_properties.py index 88697ee152..e90b23a4cf 100644 --- a/qutip/tests/core/data/test_properties.py +++ b/qutip/tests/core/data/test_properties.py @@ -3,6 +3,8 @@ from qutip import data as _data from qutip import CoreOptions +from . import conftest +from qutip.core.data.dia import clean_dia @pytest.fixture(params=[_data.CSR, _data.Dense, _data.Dia], ids=["CSR", "Dense", "Dia"]) def datatype(request): @@ -177,3 +179,67 @@ def test_isdiag(self, shape, datatype): mat[1, 0] = 1 data = _data.to(datatype, _data.Dense(mat)) assert not _data.isdiag(data) + + +class TestIsEqual: + def op_numpy(self, left, right, atol, rtol): + return np.allclose(left.to_array(), right.to_array(), rtol, atol) + + def rand_dense(shape): + return conftest.random_dense(shape, False) + + def rand_diag(shape): + return conftest.random_diag(shape, 0.5, True) + + def rand_csr(shape): + return conftest.random_csr(shape, 0.5, True) + + @pytest.mark.parametrize("factory", [rand_dense, rand_diag, rand_csr]) + @pytest.mark.parametrize("shape", [(1, 20), (20, 20), (20, 2)]) + def test_same_shape(self, factory, shape): + atol = 1e-8 + rtol = 1e-6 + A = factory(shape) + B = factory(shape) + assert _data.isequal(A, A, atol, rtol) + assert _data.isequal(B, B, atol, rtol) + assert ( + _data.isequal(A, B, atol, rtol) == self.op_numpy(A, B, atol, rtol) + ) + + @pytest.mark.parametrize("factory", [rand_dense, rand_diag, rand_csr]) + @pytest.mark.parametrize("shapeA", [(1, 10), (9, 9), (10, 2)]) + @pytest.mark.parametrize("shapeB", [(1, 9), (10, 10), (10, 1)]) + def test_different_shape(self, factory, shapeA, shapeB): + A = factory(shapeA) + B = factory(shapeB) + assert not _data.isequal(A, B, np.inf, np.inf) + + @pytest.mark.parametrize("rtol", [1e-6, 100]) + @pytest.mark.parametrize("factory", [rand_dense, rand_diag, rand_csr]) + @pytest.mark.parametrize("shape", [(1, 20), (20, 20), (20, 2)]) + def test_rtol(self, factory, shape, rtol): + mat = factory(shape) + assert _data.isequal(mat + mat * (rtol / 10), mat, 1e-14, rtol) + assert not _data.isequal(mat * (1 + rtol * 10), mat, 1e-14, rtol) + + @pytest.mark.parametrize("atol", [1e-14, 1e-6, 100]) + @pytest.mark.parametrize("factory", [rand_dense, rand_diag, rand_csr]) + @pytest.mark.parametrize("shape", [(1, 20), (20, 20), (20, 2)]) + def test_atol(self, factory, shape, atol): + A = factory(shape) + B = factory(shape) + assert _data.isequal(A, A + B * (atol / 10), atol, 0) + assert not _data.isequal(A, A + B * (atol * 10), atol, 0) + + @pytest.mark.parametrize("shape", [(1, 20), (20, 20), (20, 2)]) + def test_csr_mismatch_sort(self, shape): + A = conftest.random_csr(shape, 0.5, False) + B = A.copy().sort_indices() + assert _data.isequal(A, B) + + @pytest.mark.parametrize("shape", [(1, 20), (20, 20), (20, 2)]) + def test_dia_mismatch_sort(self, shape): + A = conftest.random_diag(shape, 0.5, False) + B = clean_dia(A) + assert _data.isequal(A, B) From e5d287750ca7cb856357a80e77328d6e126088dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Mon, 13 May 2024 10:10:37 -0400 Subject: [PATCH 177/305] Apply suggestions from code review Co-authored-by: Simon Cross --- qutip/core/qobj.py | 2 +- qutip/entropy.py | 1 - qutip/tests/test_entropy.py | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index d6c0284930..64669b3593 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -374,7 +374,7 @@ def to(self, data_type, copy=False): Parameters ---------- data_type : type, str - The data-layer type or it's string alias that the data of this + The data-layer type or its string alias that the data of this :class:`Qobj` should be converted to. copy : Bool diff --git a/qutip/entropy.py b/qutip/entropy.py index a2cde5d33b..6f3f5cc7a6 100644 --- a/qutip/entropy.py +++ b/qutip/entropy.py @@ -372,6 +372,5 @@ def entangling_power(U): a = tensor(U, U).dag() * swap13 * tensor(U, U) * swap13 Uswap = swap() * U b = tensor(Uswap, Uswap).dag() * swap13 * tensor(Uswap, Uswap) * swap13 - print(a.tr(), b.tr()) return 5.0/9 - 1.0/36 * (a.tr() + b.tr()).real diff --git a/qutip/tests/test_entropy.py b/qutip/tests/test_entropy.py index b0c4967f4e..ecf465c9bf 100644 --- a/qutip/tests/test_entropy.py +++ b/qutip/tests/test_entropy.py @@ -209,6 +209,4 @@ def test_triangle_inequality_4_qubits(self): np.sin(np.pi*_alpha)**2 / 6, id="SWAP(alpha)"), ]) def test_entangling_power(gate, expected): - print("_alpha", _alpha) - print("gate", gate) assert abs(qutip.entangling_power(gate) - expected) < 1e-12 From 8db77016e0258ebe322de72e591789d9bd6d7574 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 13 May 2024 10:47:51 -0400 Subject: [PATCH 178/305] Update from github comments --- qutip/core/data/csr.pyx | 2 ++ qutip/core/data/dense.pyx | 2 ++ qutip/core/data/dia.pyx | 2 ++ qutip/core/data/make.py | 6 +----- qutip/utilities.py | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index 385c0ebe10..2f90d50310 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -100,6 +100,8 @@ cdef class CSR(base.Data): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 3: raise ValueError("arg must be a (data, col_index, row_index) tuple") + if np.lib.NumpyVersion(np.__version__) < '2.0.0b1': + copy = bool(copy) data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') col_index = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') row_index = np.array(arg[2], dtype=idxint_dtype, copy=copy, order='C') diff --git a/qutip/core/data/dense.pyx b/qutip/core/data/dense.pyx index 723ad054c9..488fddf243 100644 --- a/qutip/core/data/dense.pyx +++ b/qutip/core/data/dense.pyx @@ -40,6 +40,8 @@ class OrderEfficiencyWarning(EfficiencyWarning): cdef class Dense(base.Data): def __init__(self, data, shape=None, copy=True): + if np.lib.NumpyVersion(np.__version__) < '2.0.0b1': + copy = bool(copy) base = np.array(data, dtype=np.complex128, order='K', copy=copy) # Ensure that the array is contiguous. # Non contiguous array with copy=False would otherwise slip through diff --git a/qutip/core/data/dia.pyx b/qutip/core/data/dia.pyx index 10c51bcf54..8e295d1b79 100644 --- a/qutip/core/data/dia.pyx +++ b/qutip/core/data/dia.pyx @@ -87,6 +87,8 @@ cdef class Dia(base.Data): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 2: raise ValueError("arg must be a (data, offsets) tuple") + if np.lib.NumpyVersion(np.__version__) < '2.0.0b1': + copy = bool(copy) data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') offsets = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') diff --git a/qutip/core/data/make.py b/qutip/core/data/make.py index c36edd304a..c7bfa8f681 100644 --- a/qutip/core/data/make.py +++ b/qutip/core/data/make.py @@ -119,11 +119,7 @@ def one_element_dia(shape, position, value=1.0): data = np.zeros((1, shape[1]), dtype=complex) data[0, position[1]] = value offsets = np.array([position[1]-position[0]]) - if np.lib.NumpyVersion(np.__version__) >= '2.0.0b1': - copy_if_needed = None - else: - copy_if_needed = False - return Dia((data, offsets), copy=copy_if_needed, shape=shape) + return Dia((data, offsets), copy=None, shape=shape) one_element = _Dispatcher(one_element_dense, name='one_element', diff --git a/qutip/utilities.py b/qutip/utilities.py index 806b874362..dfd8f39932 100644 --- a/qutip/utilities.py +++ b/qutip/utilities.py @@ -108,6 +108,7 @@ def clebsch(j1, j2, j3, m1, m2, m3): C = np.sqrt((2.0 * j3 + 1.0)*_to_long(c_factor)) s_factors = np.zeros(((vmax + 1 - vmin), (int(j1 + j2 + j3))), np.int32) + # `S` and `C` are large integer, if `sign` is a np.int32 it could oveflow sign = int((-1) ** (vmin + j2 + m2)) for i,v in enumerate(range(vmin, vmax + 1)): factor = s_factors[i,:] From 78ca4a27e90a946e8534d159c224c9f2b80b26e2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 13 May 2024 12:24:57 -0400 Subject: [PATCH 179/305] Use builtins --- qutip/core/data/csr.pyx | 5 +++-- qutip/core/data/dense.pyx | 5 +++-- qutip/core/data/dia.pyx | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index 2f90d50310..aac63a8316 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -12,7 +12,7 @@ from cpython cimport mem import numbers import warnings - +import builtins import numpy as np cimport numpy as cnp import scipy.sparse @@ -101,7 +101,8 @@ cdef class CSR(base.Data): if len(arg) != 3: raise ValueError("arg must be a (data, col_index, row_index) tuple") if np.lib.NumpyVersion(np.__version__) < '2.0.0b1': - copy = bool(copy) + # np2 accept None which act as np1's False + copy = builtins.bool(copy) data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') col_index = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') row_index = np.array(arg[2], dtype=idxint_dtype, copy=copy, order='C') diff --git a/qutip/core/data/dense.pyx b/qutip/core/data/dense.pyx index 488fddf243..22c4360aa1 100644 --- a/qutip/core/data/dense.pyx +++ b/qutip/core/data/dense.pyx @@ -5,7 +5,7 @@ from libc.string cimport memcpy cimport cython import numbers - +import builtins import numpy as np cimport numpy as cnp from scipy.linalg cimport cython_blas as blas @@ -41,7 +41,8 @@ class OrderEfficiencyWarning(EfficiencyWarning): cdef class Dense(base.Data): def __init__(self, data, shape=None, copy=True): if np.lib.NumpyVersion(np.__version__) < '2.0.0b1': - copy = bool(copy) + # np2 accept None which act as np1's False + copy = builtins.bool(copy) base = np.array(data, dtype=np.complex128, order='K', copy=copy) # Ensure that the array is contiguous. # Non contiguous array with copy=False would otherwise slip through diff --git a/qutip/core/data/dia.pyx b/qutip/core/data/dia.pyx index 8e295d1b79..6dd8bd62e6 100644 --- a/qutip/core/data/dia.pyx +++ b/qutip/core/data/dia.pyx @@ -12,7 +12,7 @@ from cpython cimport mem import numbers import warnings - +import builtins import numpy as np cimport numpy as cnp import scipy.sparse @@ -81,14 +81,14 @@ cdef class Dia(base.Data): "shapes do not match: ", str(shape), " and ", str(arg.shape), ])) shape = arg.shape - # arg = (arg.data, arg.offsets) if not isinstance(arg, tuple): raise TypeError("arg must be a scipy matrix or tuple") if len(arg) != 2: raise ValueError("arg must be a (data, offsets) tuple") if np.lib.NumpyVersion(np.__version__) < '2.0.0b1': - copy = bool(copy) + # np2 accept None which act as np1's False + copy = builtins.bool(copy) data = np.array(arg[0], dtype=np.complex128, copy=copy, order='C') offsets = np.array(arg[1], dtype=idxint_dtype, copy=copy, order='C') From a8b95a98dd9789dafcc8b44be5228ae1982dfe69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Mon, 13 May 2024 12:51:47 -0400 Subject: [PATCH 180/305] Update doc/development/contributing.rst Co-authored-by: Simon Cross --- doc/development/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/contributing.rst b/doc/development/contributing.rst index 3b376715e7..ecdbae1d18 100644 --- a/doc/development/contributing.rst +++ b/doc/development/contributing.rst @@ -122,7 +122,7 @@ QuTiP's approach is such: - Type hints are *hints* for the users. - Type hints can show the preferred usage over real implementation, for example: - ``Qobj.__mul__`` is typed to support product with scalar, not other ``Qobj``, for which ``__matmul__`` should is preferred. - - ``solver.options`` claim it return a dict not ``_SolverOptions`` (which is a subclass of dict). + - ``solver.options`` claims it return a dict not ``_SolverOptions`` (which is a subclass of dict). - Type alias are added to ``qutip.typing``. - `Any` can be used for input which type can be extended by plugin modules, (``qutip-cupy``, ``qutip-jax``, etc.) From e806272f6a7084e9e97c0a1271b93d6bf8c1eade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Mon, 13 May 2024 15:22:12 -0400 Subject: [PATCH 181/305] Update qutip/utilities.py --- qutip/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/utilities.py b/qutip/utilities.py index dfd8f39932..46a028a5da 100644 --- a/qutip/utilities.py +++ b/qutip/utilities.py @@ -108,7 +108,7 @@ def clebsch(j1, j2, j3, m1, m2, m3): C = np.sqrt((2.0 * j3 + 1.0)*_to_long(c_factor)) s_factors = np.zeros(((vmax + 1 - vmin), (int(j1 + j2 + j3))), np.int32) - # `S` and `C` are large integer, if `sign` is a np.int32 it could oveflow + # `S` and `C` are large integer,s if `sign` is a np.int32 it could oveflow sign = int((-1) ** (vmin + j2 + m2)) for i,v in enumerate(range(vmin, vmax + 1)): factor = s_factors[i,:] From 8e2bd9cf1b8bcad91f8147960c2c4feb81337009 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 13 May 2024 15:38:18 -0400 Subject: [PATCH 182/305] Comparing non sorted --- qutip/core/data/properties.pyx | 145 ++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 66 deletions(-) diff --git a/qutip/core/data/properties.pyx b/qutip/core/data/properties.pyx index b5f36f9414..2ab25ca543 100644 --- a/qutip/core/data/properties.pyx +++ b/qutip/core/data/properties.pyx @@ -7,7 +7,7 @@ from cpython cimport mem from qutip.settings import settings from qutip.core.data.base cimport idxint -from qutip.core.data cimport csr, dense, CSR, Dense, Dia +from qutip.core.data cimport csr, dense, dia, CSR, Dense, Dia from qutip.core.data.adjoint cimport transpose_csr import numpy as np @@ -326,46 +326,51 @@ cpdef bint iszero_dense(Dense matrix, double tol=-1) nogil: return True -cpdef bint isequal_dia(Dia A, Dia B, double atol=-1, double rtol=-1) nogil: +cpdef bint isequal_dia(Dia A, Dia B, double atol=-1, double rtol=-1): if A.shape[0] != B.shape[0] or A.shape[1] != B.shape[1]: return False - if atol < 0 or rtol < 0: - # Claim the gil only once - with gil: - if atol < 0: - atol = settings.core["atol"] - if rtol < 0: - rtol = settings.core["rtol"] + if atol < 0: + atol = settings.core["atol"] + if rtol < 0: + rtol = settings.core["rtol"] cdef idxint diag_a=0, diag_b=0 cdef double complex *ptr_a cdef double complex *ptr_b cdef idxint size=A.shape[1] + # TODO: + # Works only for a sorted offsets list. + # We don't have a check for whether it's already sorted, but it should be + # in most cases. Could be improved by tracking whether it is or not. + A = dia.clean_dia(A) + B = dia.clean_dia(B) + ptr_a = A.data ptr_b = B.data - while diag_a < A.num_diag and diag_b < B.num_diag: - if A.offsets[diag_a] == B.offsets[diag_b]: - for i in range(size): - if not _feq(ptr_a[i], ptr_b[i], atol, rtol): - return False - ptr_a += size - diag_a += 1 - ptr_b += size - diag_b += 1 - elif A.offsets[diag_a] <= B.offsets[diag_b]: - for i in range(size): - if not _feq(ptr_a[i], 0., atol, rtol): - return False - ptr_a += size - diag_a += 1 - else: - for i in range(size): - if not _feq(0., ptr_b[i], atol, rtol): - return False - ptr_b += size - diag_b += 1 + with nogil: + while diag_a < A.num_diag and diag_b < B.num_diag: + if A.offsets[diag_a] == B.offsets[diag_b]: + for i in range(size): + if not _feq(ptr_a[i], ptr_b[i], atol, rtol): + return False + ptr_a += size + diag_a += 1 + ptr_b += size + diag_b += 1 + elif A.offsets[diag_a] <= B.offsets[diag_b]: + for i in range(size): + if not _feq(ptr_a[i], 0., atol, rtol): + return False + ptr_a += size + diag_a += 1 + else: + for i in range(size): + if not _feq(0., ptr_b[i], atol, rtol): + return False + ptr_b += size + diag_b += 1 return True @@ -379,46 +384,54 @@ cpdef bint isequal_dense(Dense A, Dense B, double atol=-1, double rtol=-1): return np.allclose(A.as_ndarray(), B.as_ndarray(), rtol, atol) -cpdef bint isequal_csr(CSR A, CSR B, double atol=-1, double rtol=-1) nogil: +cpdef bint isequal_csr(CSR A, CSR B, double atol=-1, double rtol=-1): if A.shape[0] != B.shape[0] or A.shape[1] != B.shape[1]: return False - if atol < 0 or rtol < 0: - # Claim the gil only once - with gil: - if atol < 0: - atol = settings.core["atol"] - if rtol < 0: - rtol = settings.core["rtol"] + if atol < 0: + atol = settings.core["atol"] + if rtol < 0: + rtol = settings.core["rtol"] cdef idxint row, ptr_a, ptr_b, ptr_a_max, ptr_b_max, col_a, col_b - cdef idxint ncols = A.shape[1] - - ptr_a_max = ptr_b_max = 0 - for row in range(A.shape[0]): - ptr_a = ptr_a_max - ptr_a_max = A.row_index[row + 1] - ptr_b = ptr_b_max - ptr_b_max = B.row_index[row + 1] - col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 - col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 - while ptr_a < ptr_a_max or ptr_b < ptr_b_max: - if col_a == col_b: - if not _feq(A.data[ptr_a], B.data[ptr_b], atol, rtol): - return False - ptr_a += 1 - ptr_b += 1 - col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 - col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 - elif col_a < col_b: - if not _feq(A.data[ptr_a], 0., atol, rtol): - return False - ptr_a += 1 - col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 - else: - if not _feq(0., B.data[ptr_b], atol, rtol): - return False - ptr_b += 1 - col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + cdef idxint ncols = A.shape[1], prev_col_a, prev_col_b + + # TODO: + # Works only for sorted indices. + # We don't have a check for whether it's already sorted, but it should be + # in most cases. + A = A.sort_indices() + B = B.sort_indices() + + with nogil: + ptr_a_max = ptr_b_max = 0 + for row in range(A.shape[0]): + ptr_a = ptr_a_max + ptr_a_max = A.row_index[row + 1] + ptr_b = ptr_b_max + ptr_b_max = B.row_index[row + 1] + col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + prev_col_a = -1 + prev_col_b = -1 + while ptr_a < ptr_a_max or ptr_b < ptr_b_max: + + if col_a == col_b: + if not _feq(A.data[ptr_a], B.data[ptr_b], atol, rtol): + return False + ptr_a += 1 + ptr_b += 1 + col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 + elif col_a < col_b: + if not _feq(A.data[ptr_a], 0., atol, rtol): + return False + ptr_a += 1 + col_a = A.col_index[ptr_a] if ptr_a < ptr_a_max else ncols + 1 + else: + if not _feq(0., B.data[ptr_b], atol, rtol): + return False + ptr_b += 1 + col_b = B.col_index[ptr_b] if ptr_b < ptr_b_max else ncols + 1 return True From c865d4efceaff5b4209132193a77d895e4d40042 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 13 May 2024 15:50:47 -0400 Subject: [PATCH 183/305] Qobj.__eq__ use the isequal specialisation --- qutip/core/qobj.py | 4 ++-- qutip/tests/core/test_qobj.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 4c498bc09a..17afd610e9 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -510,8 +510,8 @@ def __eq__(self, other): return True if not isinstance(other, Qobj) or self._dims != other._dims: return False - return _data.iszero(_data.sub(self._data, other._data), - tol=settings.core['atol']) + # isequal uses both atol and rtol from settings.core + return _data.isequal(self._data, other._data) def __pow__(self, n, m=None): # calculates powers of Qobj if ( diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py index 46a7f6e8a3..4519412067 100644 --- a/qutip/tests/core/test_qobj.py +++ b/qutip/tests/core/test_qobj.py @@ -442,6 +442,15 @@ def test_QobjEquals(): q2 = qutip.Qobj(-data) assert q1 != q2 + # data's entry are of order 1, + with qutip.CoreOptions(atol=10): + assert q1 == q2 + assert q1 != q2 * 100 + + with qutip.CoreOptions(rtol=10): + assert q1 == q2 + assert q1 == q2 * 100 + def test_QobjGetItem(): "qutip.Qobj getitem" @@ -1273,4 +1282,4 @@ def test_qobj_dtype(dtype): @pytest.mark.parametrize('dtype', ["CSR", "Dense", "Dia"]) def test_dtype_in_info_string(dtype): obj = qutip.qeye(2, dtype=dtype) - assert dtype.lower() in str(obj).lower() \ No newline at end of file + assert dtype.lower() in str(obj).lower() From be004f2d6a5fd41ee35a1df453f34d935c646984 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 14 May 2024 06:41:06 -0400 Subject: [PATCH 184/305] Fix multiply test --- qutip/core/data/matmul.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutip/core/data/matmul.pyx b/qutip/core/data/matmul.pyx index 5f092b0cb7..efaa1047a6 100644 --- a/qutip/core/data/matmul.pyx +++ b/qutip/core/data/matmul.pyx @@ -527,6 +527,10 @@ cpdef CSR multiply_csr(CSR left, CSR right): + " and " + str(right.shape) ) + + left = left.sort_indices() + right = right.sort_indices() + cdef idxint col_left, left_nnz = csr.nnz(left) cdef idxint col_right, right_nnz = csr.nnz(right) cdef idxint ptr_left, ptr_right, ptr_left_max, ptr_right_max From 6c53741da24724a3a26e7689a867e816663d1258 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 14 May 2024 07:26:14 -0400 Subject: [PATCH 185/305] Update minimum tested version --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 364d6d1d64..c920904c90 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -49,7 +49,7 @@ jobs: # numpy 1.X at runtime. - case-name: numpy2_to_1 os: ubuntu-latest - python-version: "3.9" + python-version: "3.10" scipy-requirement: "" numpy-requirement: "==2.0.0rc1" roll_back_numpy: 1 From 23042b3329874c9979509bd388b3c599507fd99b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 14 May 2024 09:26:51 -0400 Subject: [PATCH 186/305] Add towncrier --- doc/changes/2425.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2425.misc diff --git a/doc/changes/2425.misc b/doc/changes/2425.misc new file mode 100644 index 0000000000..dbb0422c8a --- /dev/null +++ b/doc/changes/2425.misc @@ -0,0 +1 @@ +Qobj.__eq__ uses core's settings rtol. From a70df87482603c632739f42752d1d72391308e6b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 16 May 2024 12:06:42 -0400 Subject: [PATCH 187/305] Only normalize if initial state is normalized. --- qutip/solver/brmesolve.py | 3 ++- qutip/solver/floquet.py | 6 ++++-- qutip/solver/heom/bofin_solvers.py | 3 ++- qutip/solver/krylovsolve.py | 3 ++- qutip/solver/mesolve.py | 3 ++- qutip/solver/sesolve.py | 3 ++- qutip/solver/solver_base.py | 13 ++++++++++++- qutip/solver/stochastic.py | 6 ++++-- qutip/tests/solver/test_mesolve.py | 12 ++++++++++++ 9 files changed, 42 insertions(+), 10 deletions(-) diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index 4b54a70e8c..ce54378cc5 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -102,7 +102,8 @@ def brmesolve(H, psi0, tlist, a_ops=(), e_ops=(), c_ops=(), On `None` the states will be saved if no expectation operators are given. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index d5d1d20b60..390029ae6e 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -535,7 +535,8 @@ def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): On `None` the states will be saved if no expectation operators are given. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. Returns ------- @@ -638,7 +639,8 @@ def fmmesolve( | Whether or not to store the density matrices in the floquet basis in ``result.floquet_states``. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/solver/heom/bofin_solvers.py b/qutip/solver/heom/bofin_solvers.py index 1061c1c492..a6ecfc321d 100644 --- a/qutip/solver/heom/bofin_solvers.py +++ b/qutip/solver/heom/bofin_solvers.py @@ -498,7 +498,8 @@ def heomsolve( - | store_ados : bool | Whether or not to store the HEOM ADOs. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/solver/krylovsolve.py b/qutip/solver/krylovsolve.py index b03c4c86bd..138835ca7e 100644 --- a/qutip/solver/krylovsolve.py +++ b/qutip/solver/krylovsolve.py @@ -70,7 +70,8 @@ def krylovsolve( On `None` the states will be saved if no expectation operators are given. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index c3162872a1..f0a43de5e4 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -107,7 +107,8 @@ def mesolve( On `None` the states will be saved if no expectation operators are given. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index f4a39afde7..97025d7010 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -82,7 +82,8 @@ def sesolve( On `None` the states will be saved if no expectation operators are given. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 5726aa6cc7..139c20b9a8 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -6,12 +6,14 @@ from .. import Qobj, QobjEvo, ket2dm from .options import _SolverOptions from ..core import stack_columns, unstack_columns +from .. import settings from .result import Result from .integrator import Integrator from ..ui.progressbar import progress_bars from ._feedback import _ExpectFeedback from time import time import warnings +import numpy as np class Solver: @@ -93,6 +95,11 @@ def _prepare_state(self, state): # anything other than dimensions. 'isherm': state.isherm and not (self.rhs.dims == state.dims) } + if state.isoper: + norm = state.tr() + else: + norm = state.norm() + self._normalized = np.abs( - 1) <= settings.core["atol"] if self.rhs.dims[1] == state.dims: return stack_columns(state.data) return state.data @@ -107,7 +114,11 @@ def _restore_state(self, data, *, copy=True): else: state = Qobj(data, **self._state_metadata, copy=copy) - if data.shape[1] == 1 and self._options['normalize_output']: + if ( + data.shape[1] == 1 + and self._options['normalize_output'] + and self._normalized + ): if state.isoper: state = state * (1 / state.tr()) else: diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 73206b4586..a139716c9d 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -359,7 +359,8 @@ def smesolve( | Whether to store results from all trajectories or just store the averages. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error @@ -483,7 +484,8 @@ def ssesolve( | Whether to store results from all trajectories or just store the averages. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. + | Normalize output state to hide ODE numerical errors. Only normalize + the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. 'tqdm' uses the python module of the same name and raise an error diff --git a/qutip/tests/solver/test_mesolve.py b/qutip/tests/solver/test_mesolve.py index fc4677bd46..4c092f1690 100644 --- a/qutip/tests/solver/test_mesolve.py +++ b/qutip/tests/solver/test_mesolve.py @@ -698,3 +698,15 @@ def f(t, A): solver = qutip.MESolver(H, c_ops=[a]) result = solver.run(psi0, np.linspace(0, 30, 301), e_ops=[qutip.num(N)]) assert np.all(result.expect[0] > 4. - tol) + + +@pytest.mark.parametrize( + 'rho0', + [qutip.sigmax(), qutip.sigmaz(), qutip.qeye(2)], + ids=["sigmax", "sigmaz", "tr=2"] +) +def test_non_normalized_dm(rho0): + H = qutip.QobjEvo(qutip.num(2)) + solver = qutip.MESolver(H, c_ops=[qutip.sigmaz()]) + result = solver.run(rho0, np.linspace(0, 1, 10), e_ops=[qutip.qeye(2)]) + np.testing.assert_allclose(result.expect[0], rho0.tr(), atol=1e-7) From da41d610a6d51076049cb64501687c009eca6715 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 16 May 2024 12:44:34 -0400 Subject: [PATCH 188/305] Add towncrier --- doc/changes/2427.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2427.misc diff --git a/doc/changes/2427.misc b/doc/changes/2427.misc new file mode 100644 index 0000000000..fe0b34cc96 --- /dev/null +++ b/doc/changes/2427.misc @@ -0,0 +1 @@ +Only normalize solver states when the initial state is already normalized. From 528200b96f8a9884753f7aa16cfe24ce5754437f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 16 May 2024 12:46:04 -0400 Subject: [PATCH 189/305] Fix cut paste typo --- qutip/solver/brmesolve.py | 2 +- qutip/solver/solver_base.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index ce54378cc5..e4c15d557c 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -102,7 +102,7 @@ def brmesolve(H, psi0, tlist, a_ops=(), e_ops=(), c_ops=(), On `None` the states will be saved if no expectation operators are given. - | normalize_output : bool - | Normalize output state to hide ODE numerical errors. Only normalize + | Normalize output state to hide ODE numerical errors. Only normalize the state if the initial state is already normalized. - | progress_bar : str {'text', 'enhanced', 'tqdm', ''} | How to present the solver progress. diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 139c20b9a8..7ae7af6692 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -99,7 +99,9 @@ def _prepare_state(self, state): norm = state.tr() else: norm = state.norm() - self._normalized = np.abs( - 1) <= settings.core["atol"] + # Use the settings atol instead of the solver one since the second + # refer to the ODE tolerance and some integrator do not use it. + self._normalized = np.abs(norm - 1) <= settings.core["atol"] if self.rhs.dims[1] == state.dims: return stack_columns(state.data) return state.data From b8e160942eb620fc9f5223eab07595067dc45518 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 16 May 2024 15:01:24 -0400 Subject: [PATCH 190/305] Make 5.0.2 changelog --- doc/changelog.rst | 30 ++++++++++++++++++++++++++++++ doc/changes/2280.bugfix | 1 - doc/changes/2371.bugfix | 3 --- doc/changes/2382.bugfix | 1 - doc/changes/2388.misc | 1 - doc/changes/2393.bugfix | 1 - doc/changes/2401.doc | 1 - doc/changes/2409.doc | 2 -- doc/changes/2413.misc | 1 - doc/changes/2425.misc | 1 - doc/changes/2427.misc | 1 - 11 files changed, 30 insertions(+), 13 deletions(-) delete mode 100644 doc/changes/2280.bugfix delete mode 100644 doc/changes/2371.bugfix delete mode 100644 doc/changes/2382.bugfix delete mode 100644 doc/changes/2388.misc delete mode 100644 doc/changes/2393.bugfix delete mode 100644 doc/changes/2401.doc delete mode 100644 doc/changes/2409.doc delete mode 100644 doc/changes/2413.misc delete mode 100644 doc/changes/2425.misc delete mode 100644 doc/changes/2427.misc diff --git a/doc/changelog.rst b/doc/changelog.rst index c515aaa5d1..0299258554 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,6 +6,36 @@ Change Log .. towncrier release notes start +QuTiP 5.0.2 (2024-05-16) +======================== + +Bug Fixes +--------- + +- Use CSR as the default for expand_operator (#2280) +- The import statement was added to import the partial_transpose function directly from the qutip module. This was done to fix the TypeError: 'module' object is not callable error. + + Added a condition that check if the input rho is a ket (a type of quantum state) and it also ensures that the negativity function can handle both kets and density operators as input. (#2371) +- Ensure that end_condition of mcsolve result doesn't say target tolerance reached when it hasn't (#2382) +- Fix two bugs in steadystate floquet solver, and adjust tests to be sensitive to this issue. (#2393) + + +Documentation +------------- + +- Correct a mistake in the doc (#2401) +- Fix #2156 + Correct a sample of code in the doc (#2409) + + +Miscellaneous +------------- + +- Better metadata management in operators creation functions (#2388) +- Implicitly set minimum python version to 3.9 (#2413) +- Qobj.__eq__ uses core's settings rtol. (#2425) +- Only normalize solver states when the initial state is already normalized. (#2427) + QuTiP 5.0.0 (2024-03-26) ======================== diff --git a/doc/changes/2280.bugfix b/doc/changes/2280.bugfix deleted file mode 100644 index d8f77bbd21..0000000000 --- a/doc/changes/2280.bugfix +++ /dev/null @@ -1 +0,0 @@ -Use CSR as the default for expand_operator \ No newline at end of file diff --git a/doc/changes/2371.bugfix b/doc/changes/2371.bugfix deleted file mode 100644 index 85875e8b6a..0000000000 --- a/doc/changes/2371.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -The import statement was added to import the partial_transpose function directly from the qutip module. This was done to fix the TypeError: 'module' object is not callable error. - -Added a condition that check if the input rho is a ket (a type of quantum state) and it also ensures that the negativity function can handle both kets and density operators as input. \ No newline at end of file diff --git a/doc/changes/2382.bugfix b/doc/changes/2382.bugfix deleted file mode 100644 index f923ff6b55..0000000000 --- a/doc/changes/2382.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ensure that end_condition of mcsolve result doesn't say target tolerance reached when it hasn't \ No newline at end of file diff --git a/doc/changes/2388.misc b/doc/changes/2388.misc deleted file mode 100644 index f976819369..0000000000 --- a/doc/changes/2388.misc +++ /dev/null @@ -1 +0,0 @@ -Better metadata management in operators creation functions \ No newline at end of file diff --git a/doc/changes/2393.bugfix b/doc/changes/2393.bugfix deleted file mode 100644 index 201651b614..0000000000 --- a/doc/changes/2393.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix two bugs in steadystate floquet solver, and adjust tests to be sensitive to this issue. \ No newline at end of file diff --git a/doc/changes/2401.doc b/doc/changes/2401.doc deleted file mode 100644 index ce9323ef99..0000000000 --- a/doc/changes/2401.doc +++ /dev/null @@ -1 +0,0 @@ -Correct a mistake in the doc \ No newline at end of file diff --git a/doc/changes/2409.doc b/doc/changes/2409.doc deleted file mode 100644 index 06b6309aeb..0000000000 --- a/doc/changes/2409.doc +++ /dev/null @@ -1,2 +0,0 @@ -Fix #2156 -Correct a sample of code in the doc \ No newline at end of file diff --git a/doc/changes/2413.misc b/doc/changes/2413.misc deleted file mode 100644 index 9c0c341c05..0000000000 --- a/doc/changes/2413.misc +++ /dev/null @@ -1 +0,0 @@ -Implicitly set minimum python version to 3.9 diff --git a/doc/changes/2425.misc b/doc/changes/2425.misc deleted file mode 100644 index dbb0422c8a..0000000000 --- a/doc/changes/2425.misc +++ /dev/null @@ -1 +0,0 @@ -Qobj.__eq__ uses core's settings rtol. diff --git a/doc/changes/2427.misc b/doc/changes/2427.misc deleted file mode 100644 index fe0b34cc96..0000000000 --- a/doc/changes/2427.misc +++ /dev/null @@ -1 +0,0 @@ -Only normalize solver states when the initial state is already normalized. From 2128749c9e53c820e312dc89936136f4324f84fb Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 16 May 2024 15:17:17 -0400 Subject: [PATCH 191/305] Add contributors names to changelog --- doc/changelog.rst | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 0299258554..1678070584 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -12,20 +12,18 @@ QuTiP 5.0.2 (2024-05-16) Bug Fixes --------- -- Use CSR as the default for expand_operator (#2280) -- The import statement was added to import the partial_transpose function directly from the qutip module. This was done to fix the TypeError: 'module' object is not callable error. - - Added a condition that check if the input rho is a ket (a type of quantum state) and it also ensures that the negativity function can handle both kets and density operators as input. (#2371) -- Ensure that end_condition of mcsolve result doesn't say target tolerance reached when it hasn't (#2382) -- Fix two bugs in steadystate floquet solver, and adjust tests to be sensitive to this issue. (#2393) +- Use CSR as the default for expand_operator (#2380, by BoxiLi) +- Fix import of the partial_transpose function. + Ensures that the negativity function can handle both kets and density operators as input. (#2371, by vikas-chaudhary-2802) +- Ensure that end_condition of mcsolve result doesn't say target tolerance reached when it hasn't (#2382, by magzpavz) +- Fix two bugs in steadystate floquet solver, and adjust tests to be sensitive to this issue. (#2393, by Neill Lambert) Documentation ------------- -- Correct a mistake in the doc (#2401) -- Fix #2156 - Correct a sample of code in the doc (#2409) +- Correct a mistake in the doc (#2401, by PositroniumJS) +- Fix #2156: Correct a sample of code in the doc (#2409, by PositroniumJS) Miscellaneous @@ -37,6 +35,19 @@ Miscellaneous - Only normalize solver states when the initial state is already normalized. (#2427) +QuTiP 5.0.1 (2024-04-03) +======================== + + +Patch update fixing small issues with v5.0.0 release + +- Fix broken links in the documentation when migrating to readthedocs +- Fix readthedocs search feature +- Add setuptools to runtime compilation requirements +- Fix mcsolve documentation for open systems +- Fix OverFlowError in progress bars + + QuTiP 5.0.0 (2024-03-26) ======================== From bbd87f7ce3a8bb536b6c8576afa162b2ef9dd878 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 17 May 2024 15:22:52 -0400 Subject: [PATCH 192/305] Update actions tools versions --- .github/workflows/build.yml | 12 ++++++------ .github/workflows/build_documentation.yml | 6 +++--- .github/workflows/tests.yml | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b3e15b5334..5d4fa605f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: OVERRIDE_VERSION: ${{ github.event.inputs.override_version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 name: Install Python @@ -90,7 +90,7 @@ jobs: zip "$zipfile" -r "$stem" rm -r "$stem" - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: sdist path: | @@ -115,7 +115,7 @@ jobs: OVERRIDE_VERSION: ${{ github.event.inputs.override_version }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 name: Install Python @@ -137,7 +137,7 @@ jobs: if [[ ! -z "$OVERRIDE_VERSION" ]]; then echo "$OVERRIDE_VERSION" > VERSION; fi python -m cibuildwheel --output-dir wheelhouse - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: wheels path: ./wheelhouse/*.whl @@ -160,7 +160,7 @@ jobs: steps: - name: Download build artifacts to local runner - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - uses: actions/setup-python@v4 name: Install Python @@ -193,7 +193,7 @@ jobs: steps: - name: Download build artifacts to local runner - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - uses: actions/setup-python@v4 name: Install Python diff --git a/.github/workflows/build_documentation.yml b/.github/workflows/build_documentation.yml index d432b5135d..f1f7a258df 100644 --- a/.github/workflows/build_documentation.yml +++ b/.github/workflows/build_documentation.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 name: Install Python @@ -43,7 +43,7 @@ jobs: # -T : display a full traceback if a Python exception occurs - name: Upload built PDF files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: qutip_pdf_docs path: doc/_build/latex/* @@ -59,7 +59,7 @@ jobs: # -T : display a full traceback if a Python exception occurs - name: Upload built HTML files - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: qutip_html_docs path: doc/_build/html/* diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 12c067acf5..db393d4e64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -107,7 +107,7 @@ jobs: python-version: "3.11" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true @@ -228,7 +228,7 @@ jobs: name: Verify Towncrier entry added runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 From 0bbe13562c4c4f43d9f229b3654810d6ad080e3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 05:32:19 +0000 Subject: [PATCH 193/305] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index a41a4c36c5..a2b96718b0 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -28,7 +28,7 @@ Pygments==2.15.0 pyparsing==3.0.9 python-dateutil==2.8.2 pytz==2023.3 -requests==2.31.0 +requests==2.32.0 scipy==1.11.4 six==1.16.0 snowballstemmer==2.2.0 From a375e0cd2b85a1dd844ce903c93df30a060d554d Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 00:09:48 +0900 Subject: [PATCH 194/305] Tidy up formatting of type hints in apidoc --- doc/conf.py | 9 +++++++++ qutip/core/coefficient.py | 3 +++ qutip/core/cy/qobjevo.pyi | 3 +++ qutip/core/qobj.py | 4 ++-- qutip/solver/mcsolve.py | 3 +++ qutip/solver/mesolve.py | 7 ++++--- qutip/solver/propagator.py | 3 +++ qutip/solver/sesolve.py | 4 +++- qutip/typing.py | 2 +- 9 files changed, 31 insertions(+), 7 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 5f0c8d3779..fcef035f06 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -359,6 +359,15 @@ def qutip_version(): autodoc_member_order = 'alphabetical' +# Makes the following types appear as their alias in the apidoc +# instead of expanding the alias +autodoc_type_aliases = { + 'CoefficientLike': 'CoefficientLike', + 'ElementType': 'ElementType', + 'QobjEvoLike': 'QobjEvoLike', + 'LayerType': 'LayerType' +} + ## EXTLINKS CONFIGURATION ###################################################### extlinks = { diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index eb254b9bdc..ea53b4fb1f 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -1,3 +1,6 @@ +from __future__ import annotations # Required for Sphinx to follow + # autodoc_type_aliases + import numpy as np from numpy.typing import ArrayLike import scipy diff --git a/qutip/core/cy/qobjevo.pyi b/qutip/core/cy/qobjevo.pyi index 2730b7f98e..63dd889514 100644 --- a/qutip/core/cy/qobjevo.pyi +++ b/qutip/core/cy/qobjevo.pyi @@ -1,3 +1,6 @@ +from __future__ import annotations # Required for Sphinx to follow + # autodoc_type_aliases + from qutip.typing import LayerType, ElementType, QobjEvoLike from qutip.core.qobj import Qobj from qutip.core.data import Data diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 591611f66f..718ff3961a 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -646,7 +646,7 @@ def dual_chan(self) -> Qobj: def norm( self, - norm: Litteral["l2", "max", "fro", "tr", "one"] = None, + norm: Literal["l2", "max", "fro", "tr", "one"] = None, kwargs: dict[str, Any] = None ) -> numbers.Number: """ @@ -991,7 +991,7 @@ def inv(self, sparse: bool = False) -> Qobj: def unit( self, inplace: bool = False, - norm: Litteral["l2", "max", "fro", "tr", "one"] = None, + norm: Literal["l2", "max", "fro", "tr", "one"] = None, kwargs: dict[str, Any] = None ) -> Qobj: """ diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 38839cb6c4..13ac4842ae 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -1,3 +1,6 @@ +from __future__ import annotations # Required for Sphinx to follow + # autodoc_type_aliases + __all__ = ['mcsolve', "MCSolver"] import numpy as np diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index f0a43de5e4..5337c0c8a0 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -3,15 +3,16 @@ equation. """ +from __future__ import annotations # Required for Sphinx to follow + # autodoc_type_aliases + __all__ = ['mesolve', 'MESolver'] -import numpy as np from numpy.typing import ArrayLike from typing import Any, Callable from time import time -from .. import (Qobj, QobjEvo, isket, liouvillian, ket2dm, lindblad_dissipator) +from .. import (Qobj, QobjEvo, liouvillian, lindblad_dissipator) from ..typing import QobjEvoLike -from ..core import stack_columns, unstack_columns from ..core import data as _data from .solver_base import Solver, _solver_deprecation from .sesolve import sesolve, SESolver diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index 2a646b3701..80de3739ea 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -1,3 +1,6 @@ +from __future__ import annotations # Required for Sphinx to follow + # autodoc_type_aliases + __all__ = ['Propagator', 'propagator', 'propagator_steadystate'] import numbers diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 97025d7010..fe3b8a0da5 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -2,9 +2,11 @@ This module provides solvers for the unitary Schrodinger equation. """ +from __future__ import annotations # Required for Sphinx to follow + # autodoc_type_aliases + __all__ = ['sesolve', 'SESolver'] -import numpy as np from numpy.typing import ArrayLike from time import time from typing import Any, Callable diff --git a/qutip/typing.py b/qutip/typing.py index 1972910a66..95009a3bf8 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -1,4 +1,4 @@ -from typing import Sequence, Union, Any, Callable, Protocol +from typing import Sequence, Union, Any, Protocol from numbers import Number, Real import numpy as np import scipy.interpolate From ebc1894ba562a2537987f085c40eb8447678fbab Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 00:12:41 +0900 Subject: [PATCH 195/305] Changelog --- doc/changes/2436.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2436.doc diff --git a/doc/changes/2436.doc b/doc/changes/2436.doc new file mode 100644 index 0000000000..1066f15917 --- /dev/null +++ b/doc/changes/2436.doc @@ -0,0 +1 @@ +Tidy up formatting of type aliases in the api documentation \ No newline at end of file From 4452093ea77bfac0b9a418ae42d10412433a7bef Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 00:27:27 +0900 Subject: [PATCH 196/305] Comment formatting --- qutip/core/coefficient.py | 4 ++-- qutip/core/cy/qobjevo.pyi | 4 ++-- qutip/solver/mcsolve.py | 4 ++-- qutip/solver/mesolve.py | 4 ++-- qutip/solver/propagator.py | 4 ++-- qutip/solver/sesolve.py | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index ea53b4fb1f..963a86a79d 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -1,5 +1,5 @@ -from __future__ import annotations # Required for Sphinx to follow - # autodoc_type_aliases +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations import numpy as np from numpy.typing import ArrayLike diff --git a/qutip/core/cy/qobjevo.pyi b/qutip/core/cy/qobjevo.pyi index 63dd889514..a9efa87e78 100644 --- a/qutip/core/cy/qobjevo.pyi +++ b/qutip/core/cy/qobjevo.pyi @@ -1,5 +1,5 @@ -from __future__ import annotations # Required for Sphinx to follow - # autodoc_type_aliases +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations from qutip.typing import LayerType, ElementType, QobjEvoLike from qutip.core.qobj import Qobj diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 13ac4842ae..98df33b6f3 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -1,5 +1,5 @@ -from __future__ import annotations # Required for Sphinx to follow - # autodoc_type_aliases +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations __all__ = ['mcsolve', "MCSolver"] diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index 5337c0c8a0..fac4de9321 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -3,8 +3,8 @@ equation. """ -from __future__ import annotations # Required for Sphinx to follow - # autodoc_type_aliases +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations __all__ = ['mesolve', 'MESolver'] diff --git a/qutip/solver/propagator.py b/qutip/solver/propagator.py index 80de3739ea..2abef1680d 100644 --- a/qutip/solver/propagator.py +++ b/qutip/solver/propagator.py @@ -1,5 +1,5 @@ -from __future__ import annotations # Required for Sphinx to follow - # autodoc_type_aliases +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations __all__ = ['Propagator', 'propagator', 'propagator_steadystate'] diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index fe3b8a0da5..87c6f771c0 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -2,8 +2,8 @@ This module provides solvers for the unitary Schrodinger equation. """ -from __future__ import annotations # Required for Sphinx to follow - # autodoc_type_aliases +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations __all__ = ['sesolve', 'SESolver'] From 42695db3078f6fb9935c9ae1761472f2dabf26b7 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 09:18:14 +0900 Subject: [PATCH 197/305] ArrayLike type alias in docs --- doc/conf.py | 3 ++- qutip/solver/krylovsolve.py | 3 +++ qutip/solver/multitraj.py | 3 +++ qutip/solver/result.py | 6 ++++-- qutip/solver/solver_base.py | 3 +++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index fcef035f06..71696eeb94 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -365,7 +365,8 @@ def qutip_version(): 'CoefficientLike': 'CoefficientLike', 'ElementType': 'ElementType', 'QobjEvoLike': 'QobjEvoLike', - 'LayerType': 'LayerType' + 'LayerType': 'LayerType', + 'ArrayLike': 'ArrayLike' } ## EXTLINKS CONFIGURATION ###################################################### diff --git a/qutip/solver/krylovsolve.py b/qutip/solver/krylovsolve.py index 138835ca7e..9b94ec3773 100644 --- a/qutip/solver/krylovsolve.py +++ b/qutip/solver/krylovsolve.py @@ -1,3 +1,6 @@ +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations + __all__ = ['krylovsolve'] from .. import QobjEvo, Qobj diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 89751bc1b5..d496dda3c8 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -1,3 +1,6 @@ +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations + from .result import TrajectoryResult from .multitrajresult import MultiTrajResult from .parallel import _get_map diff --git a/qutip/solver/result.py b/qutip/solver/result.py index e7b93fdbf1..7b019381a3 100644 --- a/qutip/solver/result.py +++ b/qutip/solver/result.py @@ -1,10 +1,12 @@ """ Class for solve function results""" +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations + from typing import TypedDict, Any, Callable import numpy as np from numpy.typing import ArrayLike -from numbers import Number -from ..core import Qobj, QobjEvo, expect, isket, ket2dm, qzero_like +from ..core import Qobj, QobjEvo, expect __all__ = [ "Result", diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 7ae7af6692..207c734dc3 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -1,3 +1,6 @@ +# Required for Sphinx to follow autodoc_type_aliases +from __future__ import annotations + __all__ = ['Solver'] from numpy.typing import ArrayLike From 18ce4cd6abab0a1f68f8bcc9d53321bc814601d1 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 14:27:13 +0900 Subject: [PATCH 198/305] Mixed initial states for generic multi-traj solver --- qutip/solver/multitraj.py | 111 +++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 89751bc1b5..ddf68d22ee 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -9,6 +9,8 @@ from numpy.random import SeedSequence from numbers import Number from typing import Any, Callable +import bisect +from operator import itemgetter __all__ = ["MultiTrajSolver"] @@ -154,7 +156,10 @@ def _initialize_run(self, state, ntraj=1, args=None, e_ops=(), 'timeout': timeout, 'num_cpus': self.options['num_cpus'], }) - state0 = self._prepare_state(state) + if isinstance(state, (list, tuple)): # mixed initial conditions + state0 = [(self._prepare_state(psi), p) for psi, p in state] + else: + state0 = self._prepare_state(state) stats['preparation time'] += time() - start_time return seeds, result, map_func, map_kw, state0 @@ -274,6 +279,45 @@ def _integrate_one_traj(self, seed, tlist, result): result.add(t, self._restore_state(state, copy=False)) return seed, result + def run_mixed( + self, + initial_conditions: list[tuple[Qobj, float]], + tlist: ArrayLike, + ntraj: int | list[int] = None, + *, + args: dict[str, Any] = None, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + timeout: float = None, + seeds: int | SeedSequence | list[int | SeedSequence] = None, + ) -> MultiTrajResult: + seeds, result, map_func, map_kw, prepared_ics = self._initialize_run( + initial_conditions, + np.sum(ntraj), + args=args, + e_ops=e_ops, + timeout=timeout, + seeds=seeds, + ) + start_time = time() + map_func( + self._run_one_traj_mixed, enumerate(seeds), + (_InitialConditions(prepared_ics), tlist, e_ops), + reduce_func=result.add, map_kw=map_kw, + progress_bar=self.options["progress_bar"], + progress_bar_kwargs=self.options["progress_kwargs"] + ) + result.stats['run time'] = time() - start_time + return result + + def _run_one_traj_mixed(self, info, ics, *args, **kwargs): + id, seed = info + state, weight = ics.get_state_and_weight(id) + + seed, result = self._run_one_traj(seed, state, *args, **kwargs) + if weight != 1: + result.add_relative_weight(weight) + return seed, result + def _read_seed(self, seed, ntraj): """ Read user provided seed(s) and produce one for each trajectory. @@ -314,3 +358,68 @@ def _get_generator(self, seed): else: generator = np.random.default_rng(seed) return generator + + + +class _InitialConditions: + def __init__(self, state_list, ntraj): + if not isinstance(ntraj, (list, tuple)): + ntraj = self._minimum_roundoff_ensemble(state_list, ntraj) + + self._state_list = state_list + self._ntraj = ntraj + self._state_selector = np.cumsum(ntraj) + self.ntraj_total = self._state_selector[-1] + + def _minimum_roundoff_ensemble(self, state_list, ntraj): + filtered_states = [(index, weight) + for index, (_, weight) in enumerate(state_list) + if weight > 0] + if len(filtered_states) > ntraj: + raise ValueError(f'{ntraj} trajectories is not enough for initial' + f'mixture of {len(filtered_states)} states') + + final_traj_counts = [] + under_consideration = [] + current_total = 0 + for index, weight in filtered_states: + guess = int(np.ceil(weight * ntraj)) + current_total += guess + if guess == 1: + final_traj_counts.append((index, guess)) + else: + ratio = guess / (weight * ntraj) + bisect.insort(under_consideration, + (index, weight, guess, ratio), + key=itemgetter(3)) + + while current_total > ntraj: + index, weight, guess, ratio = under_consideration.pop() + guess -= 1 + current_total -= 1 + if guess == 1: + final_traj_counts.append((index, guess)) + else: + ratio = guess / (weight * ntraj) + bisect.insort(under_consideration, + (index, weight, guess, ratio), + key=itemgetter(3)) + + result = [0] * len(state_list) + for index, count in final_traj_counts: + result[index] = count + for index, _, count, _ in under_consideration: + result[index] = count + return result + + def get_state_and_weight(self, id): + state_index = bisect.bisect(self._state_selector, id) + if id < 0 or state_index >= len(self._state_list): + raise IndexError(f'State id {id} must be smaller than number of ' + f'trajectories {self.ntraj_total}') + + state, target_weight = self._state_list[state_index] + state_frequency = self._ntraj[state_index] / self.ntraj_total + correction_weight = target_weight / state_frequency + + return state, correction_weight From 1a3e870f2607f8277b84c9ab9cfc8360c98d36fe Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 14:53:26 +0900 Subject: [PATCH 199/305] Mixed ICs for mcsolve and nm_mcsolve --- qutip/solver/mcsolve.py | 33 +++++++++++++++++++++++++++++++-- qutip/solver/multitraj.py | 2 +- qutip/solver/nm_mcsolve.py | 20 ++++++++++++++++++-- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 38839cb6c4..08163b6b44 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -13,6 +13,7 @@ import qutip.core.data as _data from time import time from typing import Any, Callable +import warnings def mcsolve( @@ -178,8 +179,15 @@ def mcsolve( ) mc = MCSolver(H, c_ops, options=options) - result = mc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - seeds=seeds, target_tol=target_tol, timeout=timeout) + if state.isket: + result = mc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, + seeds=seeds, target_tol=target_tol, timeout=timeout) + else: + if target_tol is not None: + warnings.warn('mcsolve does not support target tolerance ' + 'for mixed initial conditions') + result = mc.run_mixed(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, + timeout=timeout, seeds=seeds) return result @@ -558,6 +566,27 @@ def run( result.stats['run time'] = time() - start_time return result + def run_mixed( + self, + initial_conditions: Qobj | list[tuple[Qobj, float]], + tlist: ArrayLike, + ntraj: int | list[int], + *, + args: dict[str, Any] = None, + e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + timeout: float = None, + seeds: int | SeedSequence | list[int | SeedSequence] = None, + ) -> McResult: + if isinstance(initial_conditions, Qobj): # decompose initial dm + eigenvalues, eigenstates = initial_conditions.eigenstates() + initial_conditions = [(state, weight) for state, weight + in zip(eigenstates, eigenvalues)] + if not self.options["improved_sampling"]: + return super().run_mixed(initial_conditions, tlist, ntraj=ntraj, + args=args, e_ops=e_ops, timeout=timeout, + seeds=seeds) + pass # TODO + def _get_integrator(self): _time_start = time() method = self.options["method"] diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index ddf68d22ee..d941614c2c 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -283,7 +283,7 @@ def run_mixed( self, initial_conditions: list[tuple[Qobj, float]], tlist: ArrayLike, - ntraj: int | list[int] = None, + ntraj: int | list[int], *, args: dict[str, Any] = None, e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 734763d07c..5c0416a85e 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -185,8 +185,13 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, ] nmmc = NonMarkovianMCSolver(H, ops_and_rates, options=options) - result = nmmc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - seeds=seeds, target_tol=target_tol, timeout=timeout) + + if state.isket: + result = nmmc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, + seeds=seeds, target_tol=target_tol, timeout=timeout) + else: + result = nmmc.run_mixed(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, + timeout=timeout, seeds=seeds) return result @@ -548,6 +553,17 @@ def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): self._martingale.reset() return result + + def run_mixed(self, initial_conditions, tlist, ntraj, *, + args=None, **kwargs): + # update `args` dictionary before precomputing martingale + self._argument(args) + + self._martingale.initialize(tlist[0], cache=tlist) + result = super().run_mixed(initial_conditions, tlist, ntraj, **kwargs) + self._martingale.reset() + + return result @property def options(self): From c21bd2becafe778537a65c30cb9c73ae8e08219a Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 23 May 2024 16:29:33 +0900 Subject: [PATCH 200/305] Mixed ICs + improved sampling --- qutip/solver/mcsolve.py | 91 +++++++++++++++++++++++++++++++++++---- qutip/solver/multitraj.py | 17 +++----- 2 files changed, 89 insertions(+), 19 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 08163b6b44..990d94b30c 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -5,7 +5,7 @@ from numpy.random import SeedSequence from ..core import QobjEvo, spre, spost, Qobj, unstack_columns from ..typing import QobjEvoLike -from .multitraj import MultiTrajSolver, _MultiTrajRHS +from .multitraj import MultiTrajSolver, _MultiTrajRHS, _InitialConditions from .solver_base import Solver, Integrator, _solver_deprecation from .multitrajresult import McResult from .mesolve import mesolve, MESolver @@ -516,6 +516,14 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): result.collapse = self._integrator.collapses return seed, result + def _no_jump_simulation(self, seed, state0, tlist, e_ops): + _, no_jump_result = self._run_one_traj(seed, state0, tlist, + e_ops, no_jump=True) + _, state, _ = self._integrator.get_state(copy=False) + no_jump_prob = self._integrator._prob_func(state) + no_jump_result.add_absolute_weight(no_jump_prob) + return no_jump_result, no_jump_prob + def run( self, state: Qobj, @@ -543,12 +551,9 @@ def run( # first run the no-jump trajectory start_time = time() - seed0, no_jump_result = self._run_one_traj(seeds[0], state0, tlist, - e_ops, no_jump=True) - _, state, _ = self._integrator.get_state(copy=False) - no_jump_prob = self._integrator._prob_func(state) - no_jump_result.add_absolute_weight(no_jump_prob) - result.add((seed0, no_jump_result)) + no_jump_result, no_jump_prob = self._no_jump_simulation( + seeds[0], state0, tlist, e_ops) + result.add((seeds[0], no_jump_result)) result.stats['no jump run time'] = time() - start_time # run the remaining trajectories with the random number floor @@ -580,12 +585,80 @@ def run_mixed( if isinstance(initial_conditions, Qobj): # decompose initial dm eigenvalues, eigenstates = initial_conditions.eigenstates() initial_conditions = [(state, weight) for state, weight - in zip(eigenstates, eigenvalues)] + in zip(eigenstates, eigenvalues) + if weight > 0] if not self.options["improved_sampling"]: return super().run_mixed(initial_conditions, tlist, ntraj=ntraj, args=args, e_ops=e_ops, timeout=timeout, seeds=seeds) - pass # TODO + + seeds, result, map_func, map_kw, prepared_ics = self._initialize_run( + initial_conditions, np.sum(ntraj), args=args, e_ops=e_ops, + timeout=timeout, seeds=seeds) + + # we need at least 2 trajectories per initial state + num_states = len(prepared_ics) + if isinstance(ntraj, (list, tuple)): + if len(ntraj) != num_states: + raise ValueError('The length of the `ntraj` list must equal ' + 'the number of states in the initial mixture') + if np.any(np.less(ntraj, 2)): + raise ValueError('For the improved sampling algorithm, at ' + 'least 2 trajectories for each member of the ' + 'initial mixture are required') + ntraj = [n - 1 for n in ntraj] + else: + if ntraj < 2 * num_states: + raise ValueError('For the improved sampling algorithm, at ' + 'least 2 trajectories for each member of the ' + 'initial mixture are required') + ntraj -= num_states + + # first run the no-jump trajectory + start_time = time() + no_jump_probs = map_func( + self._no_jump_simulation, zip(seeds, prepared_ics), + task_args=(tlist, e_ops, result), map_kw=map_kw, + progress_bar=self.options["progress_bar"], + progress_bar_kwargs=self.options["progress_kwargs"]) + result.stats['no jump run time'] = time() - start_time + + # run the remaining trajectories with the random number floor + # set to the no jump probability such that we only sample + # trajectories with jumps + start_time = time() + map_func( + self._run_one_traj_mixed_improved_sampling, + enumerate(seeds[num_states:]), + task_args=(_InitialConditions(prepared_ics, ntraj), no_jump_probs, tlist, e_ops), + task_kwargs={'no_jump': False}, + reduce_func=result.add, map_kw=map_kw, + progress_bar=self.options["progress_bar"], + progress_bar_kwargs=self.options["progress_kwargs"] + ) + result.stats['run time'] = time() - start_time + return result + + def _no_jump_simulation_mixed(self, info, tlist, e_ops, result): + seed, (state0, weight) = info + no_jump_result, no_jump_prob = self._no_jump_simulation( + seed, state0, tlist, e_ops) + no_jump_result.add_relative_weight(weight) + result.add((seed, no_jump_result)) + return no_jump_prob + + def _run_one_traj_mixed_improved_sampling(self, info, ics, no_jump_probs, + *args, **kwargs): + id, seed = info + state, weight = ics.get_state_and_weight(id) + kwargs.update( + {'jump_prob_floor': no_jump_probs[ics.get_state_index(id)]} + ) + + seed, result = self._run_one_traj(seed, state, *args, **kwargs) + if weight != 1: + result.add_relative_weight(weight) + return seed, result def _get_integrator(self): _time_start = time() diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index d941614c2c..622940162a 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -291,17 +291,12 @@ def run_mixed( seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> MultiTrajResult: seeds, result, map_func, map_kw, prepared_ics = self._initialize_run( - initial_conditions, - np.sum(ntraj), - args=args, - e_ops=e_ops, - timeout=timeout, - seeds=seeds, - ) + initial_conditions, np.sum(ntraj), args=args, e_ops=e_ops, + timeout=timeout, seeds=seeds) start_time = time() map_func( self._run_one_traj_mixed, enumerate(seeds), - (_InitialConditions(prepared_ics), tlist, e_ops), + (_InitialConditions(prepared_ics, ntraj), tlist, e_ops), reduce_func=result.add, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"] @@ -412,14 +407,16 @@ def _minimum_roundoff_ensemble(self, state_list, ntraj): result[index] = count return result - def get_state_and_weight(self, id): + def get_state_index(self, id): state_index = bisect.bisect(self._state_selector, id) if id < 0 or state_index >= len(self._state_list): raise IndexError(f'State id {id} must be smaller than number of ' f'trajectories {self.ntraj_total}') + return state_index + def get_state_and_weight(self, id): + state_index = self.get_state_index(id) state, target_weight = self._state_list[state_index] state_frequency = self._ntraj[state_index] / self.ntraj_total correction_weight = target_weight / state_frequency - return state, correction_weight From 5cbabdcec777e7cb697b575af00850a211d8375d Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 24 May 2024 14:53:25 +0900 Subject: [PATCH 201/305] Refactoring / documentation WIP --- qutip/solver/mcsolve.py | 86 ++++++++++++++++++----------- qutip/solver/multitraj.py | 110 ++++++++++++++++++++++++++++++-------- 2 files changed, 144 insertions(+), 52 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 990d94b30c..865210f028 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -503,6 +503,29 @@ def _initialize_stats(self): "num_collapse": self._num_collapse, }) return stats + + def _no_jump_probability(self, state0, tlist, seed=None, *, + result=None, e_ops=None, extra_weight=1): + """ + Simulates the no-jump trajectory from the initial state `state0`. + From the result, extracts and returns the probability of finding no + jumps until the time `tlist[-1]`. + If a MultiTrajResult `result` is provided, the simulated trajectory + will be added to that result with the given (relative) extra weight. + A seed may be provided, but is expected to be ignored by the integrator + for the no-jump simulation. + """ + _, no_jump_result = self._run_one_traj( + seed, state0, tlist, e_ops, no_jump=True) + _, state, _ = self._integrator.get_state(copy=False) + no_jump_prob = self._integrator._prob_func(state) + + if result is not None: + no_jump_result.add_absolute_weight(no_jump_prob) + no_jump_result.add_relative_weight(extra_weight) + result.add((seed, no_jump_result)) + + return no_jump_prob def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ @@ -516,13 +539,18 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): result.collapse = self._integrator.collapses return seed, result - def _no_jump_simulation(self, seed, state0, tlist, e_ops): - _, no_jump_result = self._run_one_traj(seed, state0, tlist, - e_ops, no_jump=True) - _, state, _ = self._integrator.get_state(copy=False) - no_jump_prob = self._integrator._prob_func(state) - no_jump_result.add_absolute_weight(no_jump_prob) - return no_jump_result, no_jump_prob + def _run_one_traj_mixed_improved_sampling(self, info, ics, no_jump_probs, + *args, **kwargs): + id, seed = info + state, weight = ics.get_state_and_weight(id) + kwargs.update( + {'jump_prob_floor': no_jump_probs[ics.get_state_index(id)]} + ) + + seed, result = self._run_one_traj(seed, state, *args, **kwargs) + if weight != 1: + result.add_relative_weight(weight) + return seed, result def run( self, @@ -551,9 +579,8 @@ def run( # first run the no-jump trajectory start_time = time() - no_jump_result, no_jump_prob = self._no_jump_simulation( - seeds[0], state0, tlist, e_ops) - result.add((seeds[0], no_jump_result)) + no_jump_prob = self._no_jump_probability(state0, tlist, seeds[0], + result=result, e_ops=e_ops) result.stats['no jump run time'] = time() - start_time # run the remaining trajectories with the random number floor @@ -639,27 +666,6 @@ def run_mixed( result.stats['run time'] = time() - start_time return result - def _no_jump_simulation_mixed(self, info, tlist, e_ops, result): - seed, (state0, weight) = info - no_jump_result, no_jump_prob = self._no_jump_simulation( - seed, state0, tlist, e_ops) - no_jump_result.add_relative_weight(weight) - result.add((seed, no_jump_result)) - return no_jump_prob - - def _run_one_traj_mixed_improved_sampling(self, info, ics, no_jump_probs, - *args, **kwargs): - id, seed = info - state, weight = ics.get_state_and_weight(id) - kwargs.update( - {'jump_prob_floor': no_jump_probs[ics.get_state_index(id)]} - ) - - seed, result = self._run_one_traj(seed, state, *args, **kwargs) - if weight != 1: - result.add_relative_weight(weight) - return seed, result - def _get_integrator(self): _time_start = time() method = self.options["method"] @@ -815,3 +821,21 @@ def StateFeedback( if raw_data: return _DataFeedback(default, open=open) return _QobjFeedback(default, open=open) + + + +class _unpack_arguments: + """ + If `f = _unpack_arguments(func, ('a', 'b'))` + then calling `f((3, 4), ...)` is equivalent to `func(a=3, b=4, ...)`. + + Useful since the map functions in `qutip.parallel` only allow one + of the parameters of the task to be variable. + """ + def __init__(self, func, argument_names): + self.func = func + self.argument_names = argument_names + + def __call__(self, args, **kwargs): + rearranged = dict(zip(self.argument_names, args)) + self.func(**rearranged, **kwargs) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 622940162a..614c4b6316 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -180,8 +180,8 @@ def run( For a ``state`` at time ``tlist[0]`` do the evolution as directed by ``rhs`` and for each time in ``tlist`` store the state and/or - expectation values in a :class:`.Result`. The evolution method and - stored results are determined by ``options``. + expectation values in a :class:`.MultiTrajResult`. The evolution method + and stored results are determined by ``options``. Parameters ---------- @@ -254,6 +254,14 @@ def run( result.stats['run time'] = time() - start_time return result + def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): + """ + Run one trajectory and return the result. + """ + result = self._initialize_run_one_traj(seed, state, tlist, e_ops, + **integrator_kwargs) + return self._integrate_one_traj(seed, tlist, result) + def _initialize_run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): result = self._trajectory_resultclass(e_ops, self.options) @@ -266,19 +274,27 @@ def _initialize_run_one_traj(self, seed, state, tlist, e_ops, result.add(tlist[0], self._restore_state(state, copy=False)) return result - def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): - """ - Run one trajectory and return the result. - """ - result = self._initialize_run_one_traj(seed, state, tlist, e_ops, - **integrator_kwargs) - return self._integrate_one_traj(seed, tlist, result) - def _integrate_one_traj(self, seed, tlist, result): for t, state in self._integrator.run(tlist): result.add(t, self._restore_state(state, copy=False)) return seed, result + def _run_one_traj_mixed(self, id, seeds, ics, + tlist, e_ops, **integrator_kwargs): + """ + The serial number `id` identifies which seed and which initial state to + use for running one trajectory. + """ + seed = seeds[id] + state, weight = ics.get_state_and_weight(id) + + seed, result = self._run_one_traj(seed, state, tlist, e_ops, + **integrator_kwargs) + + if weight != 1: + result.add_relative_weight(weight) + return seed, result + def run_mixed( self, initial_conditions: list[tuple[Qobj, float]], @@ -290,13 +306,72 @@ def run_mixed( timeout: float = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> MultiTrajResult: + """ + Do the evolution of the Quantum system with a mixed initial state. + + The evolution is done as directed by ``rhs``. For each time in + ``tlist``, stores the state and/or expectation values in a + :class:`.MultiTrajResult`. The evolution method and stored results are + determined by ``options``. + + Parameters + ---------- + initial_conditions : list of (:obj:`.Qobj`, float) + Statistical ensemble at the beginning of the evolution. The first + element of each tuple is a state contributing to the mixture, and + the second element is its weight, i.e., a number between 0 and 1 + describing the fraction of the ensemble in that state. The sum of + all weights is assumed to be one. + + tlist : list of double + Time for which to save the results (state and/or expect) of the + evolution. The first element of the list is the initial time of the + evolution. Time in the list must be in increasing order, but does + not need to be uniformly distributed. + + ntraj : {int, list of int} + Number of trajectories to add. If a single number is provided, this + will be the total number of trajectories, which are distributed + over the initial ensemble automatically. This parameter may also be + a list of numbers with the same number of entries as in + `initial_conditions`, specifying the number of trajectories for + each initial state explicitly. + + args : dict, optional + Change the ``args`` of the rhs for the evolution. + + e_ops : list + list of Qobj or QobjEvo to compute the expectation values. + Alternatively, function[s] with the signature f(t, state) -> expect + can be used. + + timeout : float, optional + Maximum time in seconds for the trajectories to run. Once this time + is reached, the simulation will end even if the number + of trajectories is less than ``ntraj``. In this case, the results + returned by this function will generally be invalid. + + seeds : {int, SeedSequence, list}, optional + Seed or list of seeds for each trajectories. + + Returns + ------- + results : :class:`.MultiTrajResult` + Results of the evolution. States and/or expect will be saved. You + can control the saved data in the options. + + .. note: + The simulation will end when the first end condition is reached + between ``ntraj`` and ``timeout``. Setting a target tolerance is + not supported with mixed initial conditions. + """ seeds, result, map_func, map_kw, prepared_ics = self._initialize_run( initial_conditions, np.sum(ntraj), args=args, e_ops=e_ops, timeout=timeout, seeds=seeds) start_time = time() map_func( - self._run_one_traj_mixed, enumerate(seeds), - (_InitialConditions(prepared_ics, ntraj), tlist, e_ops), + self._run_one_traj_mixed, range(len(seeds)), + (seeds, _InitialConditions(prepared_ics, ntraj), tlist, e_ops), reduce_func=result.add, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"] @@ -304,15 +379,6 @@ def run_mixed( result.stats['run time'] = time() - start_time return result - def _run_one_traj_mixed(self, info, ics, *args, **kwargs): - id, seed = info - state, weight = ics.get_state_and_weight(id) - - seed, result = self._run_one_traj(seed, state, *args, **kwargs) - if weight != 1: - result.add_relative_weight(weight) - return seed, result - def _read_seed(self, seed, ntraj): """ Read user provided seed(s) and produce one for each trajectory. @@ -367,6 +433,8 @@ def __init__(self, state_list, ntraj): self.ntraj_total = self._state_selector[-1] def _minimum_roundoff_ensemble(self, state_list, ntraj): + # https://stackoverflow.com/questions/792460/how-to-round-floats-to-integers-while-preserving-their-sum/792490#792490 + # Tests: https://github.com/pmenczel/Pseudomode-Examples/blob/main/tests/test_ic_generator.py filtered_states = [(index, weight) for index, (_, weight) in enumerate(state_list) if weight > 0] From 646a76ad4e6d9edaed0f91899351e9800fe3fcec Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Fri, 24 May 2024 16:03:29 +0900 Subject: [PATCH 202/305] Refactoring / documentation --- qutip/solver/mcsolve.py | 119 +++++++++++++++++++++++++++++--------- qutip/solver/multitraj.py | 102 ++++++++++++++++++++++++-------- 2 files changed, 169 insertions(+), 52 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 296ed9a657..995062582d 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -542,19 +542,6 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): result.collapse = self._integrator.collapses return seed, result - def _run_one_traj_mixed_improved_sampling(self, info, ics, no_jump_probs, - *args, **kwargs): - id, seed = info - state, weight = ics.get_state_and_weight(id) - kwargs.update( - {'jump_prob_floor': no_jump_probs[ics.get_state_index(id)]} - ) - - seed, result = self._run_one_traj(seed, state, *args, **kwargs) - if weight != 1: - result.add_relative_weight(weight) - return seed, result - def run( self, state: Qobj, @@ -612,11 +599,80 @@ def run_mixed( timeout: float = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> McResult: - if isinstance(initial_conditions, Qobj): # decompose initial dm + """ + Do the evolution of the Quantum system with a mixed initial state. + + The evolution is done as directed by ``rhs``. For each time in + ``tlist``, stores the state and/or expectation values in a + :class:`.MultiTrajResult`. The evolution method and stored results are + determined by ``options``. + + Parameters + ---------- + initial_conditions : {:obj:`.Qobj`, list of (:obj:`.Qobj`, float)} + Statistical ensemble at the beginning of the evolution. May be + provided either as a density matrix, or as a list of tuples. In the + latter case, the first element of each tuple is a pure state, and + the second element is its weight, i.e., a number between 0 and 1 + describing the fraction of the ensemble in that state. The sum of + all weights is assumed to be one. + + tlist : list of double + Time for which to save the results (state and/or expect) of the + evolution. The first element of the list is the initial time of the + evolution. Time in the list must be in increasing order, but does + not need to be uniformly distributed. + + ntraj : {int, list of int} + Number of trajectories to add. If a single number is provided, this + will be the total number of trajectories, which are distributed + over the initial ensemble automatically. + If `inditial_conditions` was specified as a list of pure states, + this parameter may also be a list of numbers with the same number + of entries, specifying manually the number of trajectories for each + pure state. + + args : dict, optional + Change the ``args`` of the rhs for the evolution. + + e_ops : list + list of Qobj or QobjEvo to compute the expectation values. + Alternatively, function[s] with the signature f(t, state) -> expect + can be used. + + timeout : float, optional + Maximum time in seconds for the trajectories to run. Once this time + is reached, the simulation will end even if the number + of trajectories is less than ``ntraj``. In this case, the results + returned by this function will generally be invalid. + + seeds : {int, SeedSequence, list}, optional + Seed or list of seeds for each trajectories. + + Returns + ------- + results : :class:`.MultiTrajResult` + Results of the evolution. States and/or expect will be saved. You + can control the saved data in the options. + + .. note: + The simulation will end when the first end condition is reached + between ``ntraj`` and ``timeout``. Setting a target tolerance is + not supported with mixed initial conditions. + """ + if isinstance(initial_conditions, Qobj): + # Decompose initial density matrix into eigenstates and eigenvalues + # In this case, we do not allow `ntraj` to be a list, since the + # order of the eigenstates is undefined + if isinstance(ntraj, (list, tuple)): + raise ValueError( + 'The `ntraj` parameter cannot be a list if the initial ' + 'conditions are given in the form of a density matrix') eigenvalues, eigenstates = initial_conditions.eigenstates() initial_conditions = [(state, weight) for state, weight in zip(eigenstates, eigenvalues) if weight > 0] + if not self.options["improved_sampling"]: return super().run_mixed(initial_conditions, tlist, ntraj=ntraj, args=args, e_ops=e_ops, timeout=timeout, @@ -626,7 +682,10 @@ def run_mixed( initial_conditions, np.sum(ntraj), args=args, e_ops=e_ops, timeout=timeout, seeds=seeds) - # we need at least 2 trajectories per initial state + # For improved sampling, we need to run at least 2 trajectories + # per initial state (the no-jump trajectory and one other). + # We reduce `ntraj` by one for each initial state to account for the + # no-jump trajectories num_states = len(prepared_ics) if isinstance(ntraj, (list, tuple)): if len(ntraj) != num_states: @@ -640,28 +699,32 @@ def run_mixed( else: if ntraj < 2 * num_states: raise ValueError('For the improved sampling algorithm, at ' - 'least 2 trajectories for each member of the ' - 'initial mixture are required') + 'least 2 trajectories for each member of the ' + 'initial mixture are required') ntraj -= num_states - # first run the no-jump trajectory + # Run the no-jump trajectories start_time = time() no_jump_probs = map_func( - self._no_jump_simulation, zip(seeds, prepared_ics), - task_args=(tlist, e_ops, result), map_kw=map_kw, - progress_bar=self.options["progress_bar"], + _unpack_arguments(self._no_jump_probability, + ('state0', 'seed', 'extra_weight')), + [(st, seed, w) for seed, (st, w) in zip(seeds, prepared_ics)], + task_kwargs={'tlist': tlist, 'e_ops': e_ops, 'result': result}, + map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"]) result.stats['no jump run time'] = time() - start_time - # run the remaining trajectories with the random number floor - # set to the no jump probability such that we only sample - # trajectories with jumps + # Run the remaining trajectories start_time = time() + ics_info = _InitialConditions(prepared_ics, ntraj) + arguments = [(id, no_jump_probs[ics_info.get_state_index(id)]) + for id in range(ics_info.ntraj_total)] map_func( - self._run_one_traj_mixed_improved_sampling, - enumerate(seeds[num_states:]), - task_args=(_InitialConditions(prepared_ics, ntraj), no_jump_probs, tlist, e_ops), - task_kwargs={'no_jump': False}, + _unpack_arguments(self._run_one_traj_mixed, + ('id', 'jump_prob_floor')), + arguments, + task_kwargs={'seeds': seeds[num_states:], 'ics': ics_info, + 'tlist': tlist, 'e_ops': e_ops, 'no_jump': False}, reduce_func=result.add, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"] diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 5423c2fabe..f9aaa703b8 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -426,68 +426,122 @@ def _get_generator(self, seed): class _InitialConditions: - def __init__(self, state_list, ntraj): + """ + Information about mixed initial conditions, and the number of trajectories + to be used for for each state in the mixed ensemble. + + Parameters + ---------- + state_list : list of (:obj:`.Qobj`, float) + A list of tuples (state, weight). We assume that all weights add up to + one. + ntraj : {int, list of int} + This parameter may be either the total number of trajectories, or a + list specifying the number of trajectories to be used per state. In the + former case, a list of trajectory numbers is generated such that the + fraction of trajectories for a given state approximates its weight as + well as possible, under the following constraints: + 1. the total number of trajectories is exactly `ntraj` + 2. there is at least one trajectory per initial state + + Attributes + ---------- + state_list : list of (:obj:`.Qobj`, float) + The provided list of states + ntraj : list of int + The number of trajectories to be used per state + ntraj_total : int + The total number of trajectories + """ + def __init__(self, + state_list: list[tuple[Qobj, float]], + ntraj: int | list[int]): if not isinstance(ntraj, (list, tuple)): ntraj = self._minimum_roundoff_ensemble(state_list, ntraj) - self._state_list = state_list - self._ntraj = ntraj + self.state_list = state_list + self.ntraj = ntraj self._state_selector = np.cumsum(ntraj) self.ntraj_total = self._state_selector[-1] - def _minimum_roundoff_ensemble(self, state_list, ntraj): - # https://stackoverflow.com/questions/792460/how-to-round-floats-to-integers-while-preserving-their-sum/792490#792490 - # Tests: https://github.com/pmenczel/Pseudomode-Examples/blob/main/tests/test_ic_generator.py + def _minimum_roundoff_ensemble(self, state_list, ntraj_total): + """ + Calculate a list ntraj from the given total number, under contraints + explained above. Algorithm based on https://stackoverflow.com/a/792490 + """ + # First we through out zero-weight states filtered_states = [(index, weight) for index, (_, weight) in enumerate(state_list) if weight > 0] - if len(filtered_states) > ntraj: - raise ValueError(f'{ntraj} trajectories is not enough for initial' - f'mixture of {len(filtered_states)} states') - - final_traj_counts = [] + if len(filtered_states) > ntraj_total: + raise ValueError(f'{ntraj_total} trajectories is not enough for ' + f'initial mixture of {len(filtered_states)} ' + 'states') + + # If the trajectory count of a state reaches one, that is final. + # Here we store the indices of the states with only one trajectory. + one_traj_states = [] + + # All other states are kept here. This is a list of + # (state index, target weight = w, + # current traj number = n, n / (w * ntraj_total) = r) + # sorted by the last entry. We first make a too large guess for n, + # then take away trajectories from the states with largest r under_consideration = [] + current_total = 0 for index, weight in filtered_states: - guess = int(np.ceil(weight * ntraj)) + guess = int(np.ceil(weight * ntraj_total)) current_total += guess if guess == 1: - final_traj_counts.append((index, guess)) + one_traj_states.append(index) else: - ratio = guess / (weight * ntraj) + ratio = guess / (weight * ntraj_total) bisect.insort(under_consideration, (index, weight, guess, ratio), key=itemgetter(3)) - while current_total > ntraj: + while current_total > ntraj_total: index, weight, guess, ratio = under_consideration.pop() guess -= 1 current_total -= 1 if guess == 1: - final_traj_counts.append((index, guess)) + one_traj_states.append(index) else: - ratio = guess / (weight * ntraj) + ratio = guess / (weight * ntraj_total) bisect.insort(under_consideration, (index, weight, guess, ratio), key=itemgetter(3)) - result = [0] * len(state_list) - for index, count in final_traj_counts: - result[index] = count + # Finally we arrange the results in a list of ntraj + ntraj = [0] * len(state_list) + for index in one_traj_states: + ntraj[index] = 1 for index, _, count, _ in under_consideration: - result[index] = count - return result + ntraj[index] = count + return ntraj def get_state_index(self, id): + """ + For the trajectory id (0 <= id < total_ntraj), returns the index of the + corresponding initial state in the `state_list`. + """ state_index = bisect.bisect(self._state_selector, id) - if id < 0 or state_index >= len(self._state_list): + if id < 0 or state_index >= len(self.state_list): raise IndexError(f'State id {id} must be smaller than number of ' f'trajectories {self.ntraj_total}') return state_index def get_state_and_weight(self, id): + """ + For the trajectory id (0 <= id < total_ntraj), returns the + corresponding initial state and a correction weight such that + correction_weight * (ntraj / ntraj_total) = weight + where ntraj is the number of trajectories used with this initial state + and weight the initially provided weight of the state in the ensemble. + """ state_index = self.get_state_index(id) - state, target_weight = self._state_list[state_index] + state, target_weight = self.state_list[state_index] state_frequency = self._ntraj[state_index] / self.ntraj_total correction_weight = target_weight / state_frequency return state, correction_weight From c53b6491830585ca28c8e60a9a9d6057789f40a6 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 27 May 2024 13:23:23 +0900 Subject: [PATCH 203/305] Bugfixes --- qutip/solver/mcsolve.py | 46 +++++++++++++++++++------------------- qutip/solver/multitraj.py | 4 ++-- qutip/solver/nm_mcsolve.py | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 995062582d..4f2652729b 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -48,7 +48,7 @@ def mcsolve( operators are to be treated deterministically. state : :class:`.Qobj` - Initial state vector. + Initial state vector or density matrix. tlist : array_like Times at which results are recorded. @@ -506,29 +506,23 @@ def _initialize_stats(self): "num_collapse": self._num_collapse, }) return stats - - def _no_jump_probability(self, state0, tlist, seed=None, *, - result=None, e_ops=None, extra_weight=1): + + def _no_jump_simulation(self, state, tlist, e_ops, seed=None): """ Simulates the no-jump trajectory from the initial state `state0`. - From the result, extracts and returns the probability of finding no - jumps until the time `tlist[-1]`. - If a MultiTrajResult `result` is provided, the simulated trajectory - will be added to that result with the given (relative) extra weight. - A seed may be provided, but is expected to be ignored by the integrator - for the no-jump simulation. + Returns a tuple of the `TrajectoryResult` describing this trajectory, + and its probability. + Note that a seed for the integrator may be provided, but is expected to + be ignored in the no-jump simulation. """ - _, no_jump_result = self._run_one_traj( - seed, state0, tlist, e_ops, no_jump=True) + seed, no_jump_result = self._run_one_traj( + seed, state, tlist, e_ops, no_jump=True) _, state, _ = self._integrator.get_state(copy=False) no_jump_prob = self._integrator._prob_func(state) - if result is not None: - no_jump_result.add_absolute_weight(no_jump_prob) - no_jump_result.add_relative_weight(extra_weight) - result.add((seed, no_jump_result)) + no_jump_result.add_absolute_weight(no_jump_prob) - return no_jump_prob + return seed, no_jump_result, no_jump_prob def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ @@ -705,13 +699,19 @@ def run_mixed( # Run the no-jump trajectories start_time = time() - no_jump_probs = map_func( - _unpack_arguments(self._no_jump_probability, - ('state0', 'seed', 'extra_weight')), - [(st, seed, w) for seed, (st, w) in zip(seeds, prepared_ics)], - task_kwargs={'tlist': tlist, 'e_ops': e_ops, 'result': result}, + no_jump_results = map_func( + _unpack_arguments(self._no_jump_simulation, ('state', 'seed')), + [(state, seed) for seed, (state, _) in zip(seeds, prepared_ics)], + task_kwargs={'tlist': tlist, 'e_ops': e_ops}, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"]) + + no_jump_probs = [] + for (seed, res, prob), (_, weight) in ( + zip(no_jump_results, prepared_ics)): + res.add_relative_weight(weight) + result.add((seed, res)) + no_jump_probs.append(prob) result.stats['no jump run time'] = time() - start_time # Run the remaining trajectories @@ -904,4 +904,4 @@ def __init__(self, func, argument_names): def __call__(self, args, **kwargs): rearranged = dict(zip(self.argument_names, args)) - self.func(**rearranged, **kwargs) + return self.func(**rearranged, **kwargs) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index f9aaa703b8..c90f04a254 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -469,7 +469,7 @@ def _minimum_roundoff_ensemble(self, state_list, ntraj_total): Calculate a list ntraj from the given total number, under contraints explained above. Algorithm based on https://stackoverflow.com/a/792490 """ - # First we through out zero-weight states + # First we throw out zero-weight states filtered_states = [(index, weight) for index, (_, weight) in enumerate(state_list) if weight > 0] @@ -542,6 +542,6 @@ def get_state_and_weight(self, id): """ state_index = self.get_state_index(id) state, target_weight = self.state_list[state_index] - state_frequency = self._ntraj[state_index] / self.ntraj_total + state_frequency = self.ntraj[state_index] / self.ntraj_total correction_weight = target_weight / state_frequency return state, correction_weight diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 5c0416a85e..41687815f4 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -44,7 +44,7 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, operators are to be treated deterministically. state : :class:`.Qobj` - Initial state vector. + Initial state vector or density matrix. tlist : array_like Times at which results are recorded. From b5af955f4413ceaa2454b073ab59ef950e4b92ad Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 27 May 2024 17:33:14 +0900 Subject: [PATCH 204/305] Added tests for mixed initial states in mcsolve --- qutip/solver/mcsolve.py | 27 +++- qutip/tests/solver/test_mcsolve.py | 173 +++++++++++++++++------ qutip/tests/solver/test_nm_mcsolve.py | 195 ++++++++++++++++++-------- 3 files changed, 290 insertions(+), 105 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 4f2652729b..2e742c8cad 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -6,7 +6,7 @@ import numpy as np from numpy.typing import ArrayLike from numpy.random import SeedSequence -from ..core import QobjEvo, spre, spost, Qobj, unstack_columns +from ..core import QobjEvo, spre, spost, Qobj, unstack_columns, qzero_like from ..typing import QobjEvoLike from .multitraj import MultiTrajSolver, _MultiTrajRHS, _InitialConditions from .solver_base import Solver, Integrator, _solver_deprecation @@ -528,9 +528,25 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): """ Run one trajectory and return the result. """ + jump_prob_floor = integrator_kwargs.get('jump_prob_floor', 0) + if jump_prob_floor == 1: + # The no-jump probability is one, but we are asked to generate + # a trajectory with at least one jump. + # This can happen when a user uses "improved sampling" with a dark + # initial state, or a mixed initial state containing a dark state. + # Our best option is to return a trajectory result containing only + # zeroes. This also ensures # that the final multi-trajectory + # result will contain the requested number of trajectories. + state_qobj = self._restore_state(state, copy=True) + zero = qzero_like(state_qobj) + result = self._trajectory_resultclass(e_ops, self.options) + result.collapse = [] + for t in tlist: + result.add(t, zero) + return seed, result + seed, result = super()._run_one_traj(seed, state, tlist, e_ops, **integrator_kwargs) - jump_prob_floor = integrator_kwargs.get('jump_prob_floor', 0) if jump_prob_floor > 0: result.add_relative_weight(1 - jump_prob_floor) result.collapse = self._integrator.collapses @@ -563,8 +579,9 @@ def run( # first run the no-jump trajectory start_time = time() - no_jump_prob = self._no_jump_probability(state0, tlist, seeds[0], - result=result, e_ops=e_ops) + seed0, no_jump_traj, no_jump_prob = ( + self._no_jump_simulation(state0, tlist, e_ops, seeds[0])) + result.add((seed0, no_jump_traj)) result.stats['no jump run time'] = time() - start_time # run the remaining trajectories with the random number floor @@ -705,6 +722,8 @@ def run_mixed( task_kwargs={'tlist': tlist, 'e_ops': e_ops}, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"]) + if None in no_jump_results: # timeout reached + return result no_jump_probs = [] for (seed, res, prob), (_, weight) in ( diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 436eec34f7..c2ccb0e53e 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -31,10 +31,11 @@ class StatesAndExpectOutputCase: """ size = 10 h = qutip.num(size) - state = qutip.basis(size, size-1) + pure_state = qutip.basis(size, size-1) + mixed_state = qutip.maximally_mixed_dm(size) times = np.linspace(0, 1, 101) e_ops = [qutip.num(size)] - ntraj = 2000 + ntraj = 500 def _assert_states(self, result, expected, tol): assert hasattr(result, 'states') @@ -52,13 +53,15 @@ def _assert_expect(self, result, expected, tol): np.testing.assert_allclose(test, expected_part, rtol=tol) @pytest.mark.parametrize("improved_sampling", [True, False]) - def test_states_and_expect(self, hamiltonian, args, c_ops, expected, tol, - improved_sampling): + def test_states_and_expect(self, hamiltonian, state, args, c_ops, + expected, tol, improved_sampling): options = {"store_states": True, "map": "serial", "improved_sampling": improved_sampling} - result = mcsolve(hamiltonian, self.state, self.times, args=args, + result = mcsolve(hamiltonian, state, self.times, args=args, c_ops=c_ops, e_ops=self.e_ops, ntraj=self.ntraj, - options=options, target_tol=0.05) + options=options, + # target_tol not supported for mixed initial state + target_tol=(0.05 if state.isket else None)) self._assert_expect(result, expected, tol) self._assert_states(result, expected, tol) @@ -70,8 +73,6 @@ class TestNoCollapse(StatesAndExpectOutputCase): """ def pytest_generate_tests(self, metafunc): tol = 1e-8 - expect = (qutip.expect(self.e_ops[0], self.state) - * np.ones_like(self.times)) hamiltonian_types = [ (self.h, "Qobj"), ([self.h], "list"), @@ -79,12 +80,22 @@ def pytest_generate_tests(self, metafunc): args={'constant': 0}), "QobjEvo"), (callable_qobj(self.h), "callable"), ] - cases = [pytest.param(hamiltonian, {}, [], [expect], tol, id=id) + cases = [pytest.param(hamiltonian, {}, [], tol, id=id) for hamiltonian, id in hamiltonian_types] metafunc.parametrize( - ['hamiltonian', 'args', 'c_ops', 'expected', 'tol'], + ['hamiltonian', 'args', 'c_ops', 'tol'], cases) + initial_state_types = [ + (self.pure_state, "pure"), + (self.mixed_state, "mixed"), + ] + expect = [qutip.expect(self.e_ops[0], state) * np.ones_like(self.times) + for state, _ in initial_state_types] + cases = [pytest.param(state, [exp], id=id) + for (state, id), exp in zip(initial_state_types, expect)] + metafunc.parametrize(['state', 'expected'], cases) + # Previously the "states_only" and "expect_only" tests were mixed in to # every other test case. We move them out into the simplest set so that # their behaviour remains tested, but isn't repeated as often to keep test @@ -92,20 +103,20 @@ def pytest_generate_tests(self, metafunc): # test cases, this is just testing the single-output behaviour. @pytest.mark.parametrize("improved_sampling", [True, False]) - def test_states_only(self, hamiltonian, args, c_ops, expected, tol, - improved_sampling): + def test_states_only(self, hamiltonian, state, args, c_ops, + expected, tol, improved_sampling): options = {"store_states": True, "map": "serial", "improved_sampling": improved_sampling} - result = mcsolve(hamiltonian, self.state, self.times, args=args, + result = mcsolve(hamiltonian, state, self.times, args=args, c_ops=c_ops, e_ops=[], ntraj=self.ntraj, options=options) self._assert_states(result, expected, tol) @pytest.mark.parametrize("improved_sampling", [True, False]) - def test_expect_only(self, hamiltonian, args, c_ops, expected, tol, - improved_sampling): + def test_expect_only(self, hamiltonian, state, args, c_ops, + expected, tol, improved_sampling): options = {'map': 'serial', "improved_sampling": improved_sampling} - result = mcsolve(hamiltonian, self.state, self.times, args=args, + result = mcsolve(hamiltonian, state, self.times, args=args, c_ops=c_ops, e_ops=self.e_ops, ntraj=self.ntraj, options=options) self._assert_expect(result, expected, tol) @@ -119,8 +130,6 @@ class TestConstantCollapse(StatesAndExpectOutputCase): def pytest_generate_tests(self, metafunc): tol = 0.25 coupling = 0.2 - expect = (qutip.expect(self.e_ops[0], self.state) - * np.exp(-coupling * self.times)) collapse_op = qutip.destroy(self.size) c_op_types = [ (np.sqrt(coupling)*collapse_op, {}, "constant"), @@ -128,12 +137,23 @@ def pytest_generate_tests(self, metafunc): (callable_qobj(collapse_op, _return_constant), {'constant': np.sqrt(coupling)}, "function"), ] - cases = [pytest.param(self.h, args, [c_op], [expect], tol, id=id) + cases = [pytest.param(self.h, args, [c_op], tol, id=id) for c_op, args, id in c_op_types] metafunc.parametrize( - ['hamiltonian', 'args', 'c_ops', 'expected', 'tol'], + ['hamiltonian', 'args', 'c_ops', 'tol'], cases) + initial_state_types = [ + (self.pure_state, "pure"), + (self.mixed_state, "mixed"), + ] + expect = [(qutip.expect(self.e_ops[0], state) + * np.exp(-coupling * self.times)) + for state, _ in initial_state_types] + cases = [pytest.param(state, [exp], id=id) + for (state, id), exp in zip(initial_state_types, expect)] + metafunc.parametrize(['state', 'expected'], cases) + class TestTimeDependentCollapse(StatesAndExpectOutputCase): """ @@ -143,8 +163,6 @@ class TestTimeDependentCollapse(StatesAndExpectOutputCase): def pytest_generate_tests(self, metafunc): tol = 0.25 coupling = 0.2 - expect = (qutip.expect(self.e_ops[0], self.state) - * np.exp(-coupling * (1 - np.exp(-self.times)))) collapse_op = qutip.destroy(self.size) collapse_args = {'constant': np.sqrt(coupling), 'rate': 0.5} collapse_string = 'sqrt({} * exp(-t))'.format(coupling) @@ -152,12 +170,23 @@ def pytest_generate_tests(self, metafunc): ([collapse_op, _return_decay], collapse_args, "function"), ([collapse_op, collapse_string], {}, "string"), ] - cases = [pytest.param(self.h, args, [c_op], [expect], tol, id=id) + cases = [pytest.param(self.h, args, [c_op], tol, id=id) for c_op, args, id in c_op_types] metafunc.parametrize( - ['hamiltonian', 'args', 'c_ops', 'expected', 'tol'], + ['hamiltonian', 'args', 'c_ops', 'tol'], cases) + initial_state_types = [ + (self.pure_state, "pure"), + (self.mixed_state, "mixed"), + ] + expect = [(qutip.expect(self.e_ops[0], state) + * np.exp(-coupling * (1 - np.exp(-self.times)))) + for state, _ in initial_state_types] + cases = [pytest.param(state, [exp], id=id) + for (state, id), exp in zip(initial_state_types, expect)] + metafunc.parametrize(['state', 'expected'], cases) + def test_stored_collapse_operators_and_times(): """ @@ -179,16 +208,21 @@ def test_stored_collapse_operators_and_times(): @pytest.mark.parametrize("improved_sampling", [True, False]) @pytest.mark.parametrize("keep_runs_results", [True, False]) -def test_states_outputs(keep_runs_results, improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_states_outputs(keep_runs_results, improved_sampling, + mixed_initial_state): # We're just testing the output value, so it's important whether certain # things are complex or real, but not what the magnitudes of constants are. focks = 5 - ntraj = 5 - a = qutip.tensor(qutip.destroy(focks), qutip.qeye(2)) - sm = qutip.tensor(qutip.qeye(focks), qutip.sigmam()) + ntraj = 13 + a = qutip.destroy(focks) & qutip.qeye(2) + sm = qutip.qeye(focks) & qutip.sigmam() H = 1j*a.dag()*sm + a H = H + H.dag() - state = qutip.basis([focks, 2], [0, 1]) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(focks) & qutip.fock_dm(2, 1) + else: + state = qutip.basis([focks, 2], [0, 1]) times = np.linspace(0, 10, 21) c_ops = [a, sm] data = mcsolve(H, state, times, c_ops, ntraj=ntraj, @@ -200,6 +234,10 @@ def test_states_outputs(keep_runs_results, improved_sampling): assert isinstance(data.average_states[0], qutip.Qobj) assert data.average_states[0].norm() == pytest.approx(1.) assert data.average_states[0].isoper + if state.isket: + assert data.average_states[0] == qutip.ket2dm(state) + else: + assert data.average_states[0] == state assert isinstance(data.average_final_state, qutip.Qobj) assert data.average_final_state.norm() == pytest.approx(1.) @@ -222,9 +260,10 @@ def test_states_outputs(keep_runs_results, improved_sampling): assert data.runs_final_states[0].norm() == pytest.approx(1.) assert data.runs_final_states[0].isket - assert isinstance(data.steady_state(), qutip.Qobj) - assert data.steady_state().norm() == pytest.approx(1.) - assert data.steady_state().isoper + steady_state = data.steady_state() + assert isinstance(steady_state, qutip.Qobj) + assert steady_state.norm() == pytest.approx(1.) + assert steady_state.isoper np.testing.assert_allclose(times, data.times) assert data.num_trajectories == ntraj @@ -237,16 +276,21 @@ def test_states_outputs(keep_runs_results, improved_sampling): @pytest.mark.parametrize("improved_sampling", [True, False]) @pytest.mark.parametrize("keep_runs_results", [True, False]) -def test_expectation_outputs(keep_runs_results, improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_expectation_outputs(keep_runs_results, improved_sampling, + mixed_initial_state): # We're just testing the output value, so it's important whether certain # things are complex or real, but not what the magnitudes of constants are. focks = 5 - ntraj = 5 - a = qutip.tensor(qutip.destroy(focks), qutip.qeye(2)) - sm = qutip.tensor(qutip.qeye(focks), qutip.sigmam()) + ntraj = 13 + a = qutip.destroy(focks) & qutip.qeye(2) + sm = qutip.qeye(focks) & qutip.sigmam() H = 1j*a.dag()*sm + a H = H + H.dag() - state = qutip.basis([focks, 2], [0, 1]) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(focks) & qutip.fock_dm(2, 1) + else: + state = qutip.basis([focks, 2], [0, 1]) times = np.linspace(0, 10, 5) c_ops = [a, sm] e_ops = [a.dag()*a, sm.dag()*sm, a] @@ -339,7 +383,7 @@ def test_bad_seed(self, improved_sampling): kwargs = {'c_ops': self.c_ops, 'ntraj': self.ntraj, "options": {"improved_sampling": improved_sampling}} with pytest.raises(ValueError): - first = mcsolve(*args, seeds=[1], **kwargs) + mcsolve(*args, seeds=[1], **kwargs) @pytest.mark.parametrize("improved_sampling", [True, False]) def test_generator(self, improved_sampling): @@ -373,12 +417,16 @@ def test_stepping(self): @pytest.mark.parametrize("improved_sampling", [True, False]) -def test_timeout(improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_timeout(improved_sampling, mixed_initial_state): size = 10 ntraj = 1000 a = qutip.destroy(size) H = qutip.num(size) - state = qutip.basis(size, size-1) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(size) + else: + state = qutip.basis(size, size-1) times = np.linspace(0, 1.0, 100) coupling = 0.5 n_th = 0.05 @@ -414,12 +462,16 @@ def test_target_tol(improved_sampling): assert res.stats['end_condition'] == 'ntraj reached' @pytest.mark.parametrize("improved_sampling", [True, False]) -def test_super_H(improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_super_H(improved_sampling, mixed_initial_state): size = 10 - ntraj = 1000 + ntraj = 250 a = qutip.destroy(size) H = qutip.num(size) - state = qutip.basis(size, size-1) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(size) + else: + state = qutip.basis(size, size-1) times = np.linspace(0, 1.0, 100) # Arbitrary coupling and bath temperature. coupling = 0.5 @@ -427,11 +479,11 @@ def test_super_H(improved_sampling): c_ops = np.sqrt(coupling * (n_th + 1)) * a e_ops = [qutip.num(size)] mc_expected = mcsolve(H, state, times, c_ops, e_ops, ntraj=ntraj, - target_tol=0.1, + target_tol=(0.1 if state.isket else None), options={'map': 'serial', "improved_sampling": improved_sampling}) mc = mcsolve(qutip.liouvillian(H), state, times, c_ops, e_ops, ntraj=ntraj, - target_tol=0.1, + target_tol=(0.1 if state.isket else None), options={'map': 'serial', "improved_sampling": improved_sampling}) np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.65) @@ -510,3 +562,34 @@ def test_feedback(func, kind): psi0, np.linspace(0, 3, 31), e_ops=[qutip.num(10)], ntraj=10 ) assert np.all(result.expect[0] > 4. - tol) + + +@pytest.mark.parametrize(["initial_state", "ntraj"], [ + pytest.param(qutip.maximally_mixed_dm(2), 5, id="dm"), + pytest.param([(qutip.basis(2, 0), 0.3), (qutip.basis(2, 1), 0.7)], + 5, id="statelist"), + pytest.param([(qutip.basis(2, 0), 0.3), (qutip.basis(2, 1), 0.7)], + [4, 2], id="ntraj-spec"), + pytest.param([(qutip.basis(2, 0), 0.3), + ((qutip.basis(2, 0) + qutip.basis(2, 1)).unit(), 0.7)], + [4, 2], id="non-orthogonals"), +]) +@pytest.mark.parametrize("improved_sampling", [True, False]) +def test_mixed_averaging(improved_sampling, initial_state, ntraj): + # we will only check that the initial state of the result equals the + # intended initial state exactly + H = qutip.sigmax() + tlist = [0, 1] + L = qutip.sigmam() + + solver = qutip.MCSolver( + H, [L], options={'improved_sampling': improved_sampling}) + result = solver.run_mixed(initial_state, tlist, ntraj) + + if isinstance(initial_state, qutip.Qobj): + reference = initial_state + else: + reference = sum(p * psi.proj() for psi, p in initial_state) + + assert result.states[0] == reference + assert result.num_trajectories == np.sum(ntraj) diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index e2e754d33e..de43d8bb6a 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -7,14 +7,20 @@ from qutip.solver.nm_mcsolve import nm_mcsolve, NonMarkovianMCSolver +@pytest.mark.slow @pytest.mark.parametrize("improved_sampling", [True, False]) -def test_agreement_with_mesolve_for_negative_rates(improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_agreement_with_mesolve_for_negative_rates( + improved_sampling, mixed_initial_state): """ A rough test that nm_mcsolve agress with mesolve in the presence of negative rates. """ times = np.linspace(0, 0.25, 51) - psi0 = qutip.basis(2, 1) + if mixed_initial_state: + state0 = qutip.maximally_mixed_dm(2) + else: + state0 = qutip.basis(2, 1) a0 = qutip.destroy(2) H = a0.dag() * a0 e_ops = [ @@ -38,7 +44,7 @@ def test_agreement_with_mesolve_for_negative_rates(improved_sampling): [a0, gamma2], ] mc_result = nm_mcsolve( - H, psi0, times, ops_and_rates, + H, state0, times, ops_and_rates, args=args, e_ops=e_ops, ntraj=1000 if improved_sampling else 2000, options={"rtol": 1e-8, "improved_sampling": improved_sampling}, seeds=0, @@ -50,7 +56,7 @@ def test_agreement_with_mesolve_for_negative_rates(improved_sampling): [qutip.lindblad_dissipator(a0, a0), gamma2], ] me_result = qutip.mesolve( - H, psi0, times, d_ops, + H, state0, times, d_ops, args=args, e_ops=e_ops, ) @@ -154,10 +160,11 @@ class StatesAndExpectOutputCase: """ size = 10 h = qutip.num(size) - state = qutip.basis(size, size-1) + pure_state = qutip.basis(size, size-1) + mixed_state = qutip.maximally_mixed_dm(size) times = np.linspace(0, 1, 101) e_ops = [qutip.num(size)] - ntraj = 2000 + ntraj = 500 def _assert_states(self, result, expected, tol): assert hasattr(result, 'states') @@ -176,16 +183,17 @@ def _assert_expect(self, result, expected, tol): @pytest.mark.parametrize("improved_sampling", [True, False]) def test_states_and_expect( - self, hamiltonian, args, ops_and_rates, expected, tol, - improved_sampling + self, hamiltonian, state, args, ops_and_rates, + expected, tol, improved_sampling ): options = {"store_states": True, "map": "serial", "improved_sampling": improved_sampling} result = nm_mcsolve( - hamiltonian, self.state, self.times, args=args, + hamiltonian, state, self.times, args=args, ops_and_rates=ops_and_rates, e_ops=self.e_ops, ntraj=self.ntraj, options=options, - target_tol=0.05, + # target_tol not supported for mixed initial state + target_tol=(0.05 if state.isket else None) ) self._assert_expect(result, expected, tol) self._assert_states(result, expected, tol) @@ -199,10 +207,6 @@ class TestNoCollapse(StatesAndExpectOutputCase): def pytest_generate_tests(self, metafunc): tol = 1e-8 - expect = ( - qutip.expect(self.e_ops[0], self.state) - * np.ones_like(self.times) - ) hamiltonian_types = [ (self.h, "Qobj"), ([self.h], "list"), @@ -212,13 +216,23 @@ def pytest_generate_tests(self, metafunc): (callable_qobj(self.h), "callable"), ] cases = [ - pytest.param(hamiltonian, {}, [], [expect], tol, id=id) + pytest.param(hamiltonian, {}, [], tol, id=id) for hamiltonian, id in hamiltonian_types ] metafunc.parametrize([ - 'hamiltonian', 'args', 'ops_and_rates', 'expected', 'tol', + 'hamiltonian', 'args', 'ops_and_rates', 'tol', ], cases) + initial_state_types = [ + (self.pure_state, "pure"), + (self.mixed_state, "mixed"), + ] + expect = [qutip.expect(self.e_ops[0], state) * np.ones_like(self.times) + for state, _ in initial_state_types] + cases = [pytest.param(state, [exp], id=id) + for (state, id), exp in zip(initial_state_types, expect)] + metafunc.parametrize(['state', 'expected'], cases) + # Previously the "states_only" and "expect_only" tests were mixed in to # every other test case. We move them out into the simplest set so that # their behaviour remains tested, but isn't repeated as often to keep test @@ -226,23 +240,23 @@ def pytest_generate_tests(self, metafunc): # test cases, this is just testing the single-output behaviour. @pytest.mark.parametrize("improved_sampling", [True, False]) - def test_states_only(self, hamiltonian, args, ops_and_rates, expected, tol, - improved_sampling): + def test_states_only(self, hamiltonian, state, args, ops_and_rates, + expected, tol, improved_sampling): options = {"store_states": True, "map": "serial", "improved_sampling": improved_sampling} result = nm_mcsolve( - hamiltonian, self.state, self.times, args=args, + hamiltonian, state, self.times, args=args, ops_and_rates=ops_and_rates, e_ops=[], ntraj=self.ntraj, options=options, ) self._assert_states(result, expected, tol) @pytest.mark.parametrize("improved_sampling", [True, False]) - def test_expect_only(self, hamiltonian, args, ops_and_rates, expected, tol, - improved_sampling): + def test_expect_only(self, hamiltonian, state, args, ops_and_rates, + expected, tol, improved_sampling): options = {'map': 'serial', "improved_sampling": improved_sampling} result = nm_mcsolve( - hamiltonian, self.state, self.times, args=args, + hamiltonian, state, self.times, args=args, ops_and_rates=ops_and_rates, e_ops=self.e_ops, ntraj=self.ntraj, options=options, ) @@ -258,10 +272,6 @@ class TestConstantCollapse(StatesAndExpectOutputCase): def pytest_generate_tests(self, metafunc): tol = 0.25 rate = 0.2 - expect = ( - qutip.expect(self.e_ops[0], self.state) - * np.exp(-rate * self.times) - ) op = qutip.destroy(self.size) op_and_rate_types = [ ([op, rate], {}, "constant"), @@ -270,13 +280,24 @@ def pytest_generate_tests(self, metafunc): ([op, lambda t, w: rate], {"w": 1.0}, "function_with_args"), ] cases = [ - pytest.param(self.h, args, [op_and_rate], [expect], tol, id=id) + pytest.param(self.h, args, [op_and_rate], tol, id=id) for op_and_rate, args, id in op_and_rate_types ] metafunc.parametrize([ - 'hamiltonian', 'args', 'ops_and_rates', 'expected', 'tol', + 'hamiltonian', 'args', 'ops_and_rates', 'tol', ], cases) + initial_state_types = [ + (self.pure_state, "pure"), + (self.mixed_state, "mixed"), + ] + expect = [(qutip.expect(self.e_ops[0], state) + * np.exp(-rate * self.times)) + for state, _ in initial_state_types] + cases = [pytest.param(state, [exp], id=id) + for (state, id), exp in zip(initial_state_types, expect)] + metafunc.parametrize(['state', 'expected'], cases) + class TestTimeDependentCollapse(StatesAndExpectOutputCase): """ @@ -287,10 +308,6 @@ class TestTimeDependentCollapse(StatesAndExpectOutputCase): def pytest_generate_tests(self, metafunc): tol = 0.25 coupling = 0.2 - expect = ( - qutip.expect(self.e_ops[0], self.state) - * np.exp(-coupling * (1 - np.exp(-self.times))) - ) op = qutip.destroy(self.size) rate_args = {'constant': coupling, 'rate': 0.5} rate_string = 'sqrt({} * exp(-t))'.format(coupling) @@ -299,13 +316,24 @@ def pytest_generate_tests(self, metafunc): ([op, _return_decay], rate_args, "function"), ] cases = [ - pytest.param(self.h, args, [op_and_rate], [expect], tol, id=id) + pytest.param(self.h, args, [op_and_rate], tol, id=id) for op_and_rate, args, id in op_and_rate_types ] metafunc.parametrize([ - 'hamiltonian', 'args', 'ops_and_rates', 'expected', 'tol', + 'hamiltonian', 'args', 'ops_and_rates', 'tol', ], cases) + initial_state_types = [ + (self.pure_state, "pure"), + (self.mixed_state, "mixed"), + ] + expect = [(qutip.expect(self.e_ops[0], state) + * np.exp(-coupling * (1 - np.exp(-self.times)))) + for state, _ in initial_state_types] + cases = [pytest.param(state, [exp], id=id) + for (state, id), exp in zip(initial_state_types, expect)] + metafunc.parametrize(['state', 'expected'], cases) + def test_stored_collapse_operators_and_times(): """ @@ -332,16 +360,21 @@ def test_stored_collapse_operators_and_times(): @pytest.mark.parametrize("improved_sampling", [True, False]) @pytest.mark.parametrize("keep_runs_results", [True, False]) -def test_states_outputs(keep_runs_results, improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_states_outputs(keep_runs_results, improved_sampling, + mixed_initial_state): # We're just testing the output value, so it's important whether certain # things are complex or real, but not what the magnitudes of constants are. focks = 5 - ntraj = 5 - a = qutip.tensor(qutip.destroy(focks), qutip.qeye(2)) - sm = qutip.tensor(qutip.qeye(focks), qutip.sigmam()) + ntraj = 13 + a = qutip.destroy(focks) & qutip.qeye(2) + sm = qutip.qeye(focks) & qutip.sigmam() H = 1j*a.dag()*sm + a H = H + H.dag() - state = qutip.basis([focks, 2], [0, 1]) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(focks) & qutip.fock_dm(2, 1) + else: + state = qutip.basis([focks, 2], [0, 1]) times = np.linspace(0, 10, 21) ops_and_rates = [ (a, 1.0), @@ -361,6 +394,10 @@ def test_states_outputs(keep_runs_results, improved_sampling): assert isinstance(data.average_states[0], qutip.Qobj) assert data.average_states[0].norm() == pytest.approx(1.) assert data.average_states[0].isoper + if state.isket: + assert data.average_states[0] == qutip.ket2dm(state) + else: + assert data.average_states[0] == state assert isinstance(data.average_final_state, qutip.Qobj) assert data.average_final_state.norm() == pytest.approx(1.) @@ -378,9 +415,10 @@ def test_states_outputs(keep_runs_results, improved_sampling): assert data.runs_final_states[0].norm() == pytest.approx(1.) assert data.runs_final_states[0].isket - assert isinstance(data.steady_state(), qutip.Qobj) - assert data.steady_state().norm() == pytest.approx(1.) - assert data.steady_state().isoper + steady_state = data.steady_state() + assert isinstance(steady_state, qutip.Qobj) + assert steady_state.norm() == pytest.approx(1.) + assert steady_state.isoper np.testing.assert_allclose(times, data.times) assert data.num_trajectories == ntraj @@ -393,16 +431,21 @@ def test_states_outputs(keep_runs_results, improved_sampling): @pytest.mark.parametrize("improved_sampling", [True, False]) @pytest.mark.parametrize("keep_runs_results", [True, False]) -def test_expectation_outputs(keep_runs_results, improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_expectation_outputs(keep_runs_results, improved_sampling, + mixed_initial_state): # We're just testing the output value, so it's important whether certain # things are complex or real, but not what the magnitudes of constants are. focks = 5 - ntraj = 5 - a = qutip.tensor(qutip.destroy(focks), qutip.qeye(2)) - sm = qutip.tensor(qutip.qeye(focks), qutip.sigmam()) + ntraj = 13 + a = qutip.destroy(focks) & qutip.qeye(2) + sm = qutip.qeye(focks) & qutip.sigmam() H = 1j*a.dag()*sm + a H = H + H.dag() - state = qutip.basis([focks, 2], [0, 1]) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(focks) & qutip.fock_dm(2, 1) + else: + state = qutip.basis([focks, 2], [0, 1]) times = np.linspace(0, 10, 5) ops_and_rates = [ (a, 1.0), @@ -529,12 +572,16 @@ def test_stepping(self): @pytest.mark.parametrize("improved_sampling", [True, False]) -def test_timeout(improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_timeout(improved_sampling, mixed_initial_state): size = 10 ntraj = 1000 a = qutip.destroy(size) H = qutip.num(size) - state = qutip.basis(size, size-1) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(size) + else: + state = qutip.basis(size, size-1) times = np.linspace(0, 1.0, 100) coupling = 0.5 n_th = 0.05 @@ -551,12 +598,16 @@ def test_timeout(improved_sampling): @pytest.mark.parametrize("improved_sampling", [True, False]) -def test_super_H(improved_sampling): +@pytest.mark.parametrize("mixed_initial_state", [True, False]) +def test_super_H(improved_sampling, mixed_initial_state): size = 10 - ntraj = 1000 + ntraj = 250 a = qutip.destroy(size) H = qutip.num(size) - state = qutip.basis(size, size-1) + if mixed_initial_state: + state = qutip.maximally_mixed_dm(size) + else: + state = qutip.basis(size, size-1) times = np.linspace(0, 1.0, 100) # Arbitrary coupling and bath temperature. coupling = 0.5 @@ -567,13 +618,13 @@ def test_super_H(improved_sampling): e_ops = [qutip.num(size)] mc_expected = nm_mcsolve( H, state, times, ops_and_rates, e_ops, ntraj=ntraj, - target_tol=0.1, options={'map': 'serial', - "improved_sampling": improved_sampling}, + target_tol=(0.1 if state.isket else None), + options={'map': 'serial', "improved_sampling": improved_sampling}, ) mc = nm_mcsolve( qutip.liouvillian(H), state, times, ops_and_rates, e_ops, ntraj=ntraj, - target_tol=0.1, options={'map': 'serial', - "improved_sampling": improved_sampling}) + target_tol=(0.1 if state.isket else None), + options={'map': 'serial', "improved_sampling": improved_sampling}) np.testing.assert_allclose(mc_expected.expect[0], mc.expect[0], atol=0.65) @@ -644,3 +695,35 @@ def test_dynamic_arguments(): H, state, times, ops_and_rates, ntraj=25, args={"collapse": []}, ) assert all(len(collapses) <= 1 for collapses in mc.col_which) + + +@pytest.mark.parametrize(["initial_state", "ntraj"], [ + pytest.param(qutip.maximally_mixed_dm(2), 5, id="dm"), + pytest.param([(qutip.basis(2, 0), 0.3), (qutip.basis(2, 1), 0.7)], + 5, id="statelist"), + pytest.param([(qutip.basis(2, 0), 0.3), (qutip.basis(2, 1), 0.7)], + [4, 2], id="ntraj-spec"), + pytest.param([(qutip.basis(2, 0), 0.3), + ((qutip.basis(2, 0) + qutip.basis(2, 1)).unit(), 0.7)], + [4, 2], id="non-orthogonals"), +]) +@pytest.mark.parametrize("improved_sampling", [True, False]) +def test_mixed_averaging(improved_sampling, initial_state, ntraj): + # we will only check that the initial state of the result equals the + # intended initial state exactly + H = qutip.sigmax() + tlist = [0, 1] + L = qutip.sigmam() + rate = -1 + + solver = qutip.NonMarkovianMCSolver( + H, [(L, rate)], options={'improved_sampling': improved_sampling}) + result = solver.run_mixed(initial_state, tlist, ntraj) + + if isinstance(initial_state, qutip.Qobj): + reference = initial_state + else: + reference = sum(p * psi.proj() for psi, p in initial_state) + + assert result.states[0] == reference + assert result.num_trajectories == np.sum(ntraj) From 1da856cacc744f259295b059dfe624c04e453396 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 27 May 2024 18:08:28 +0900 Subject: [PATCH 205/305] Changelog, formatting --- doc/changes/2437.feature | 1 + qutip/solver/mcsolve.py | 4 +--- qutip/solver/multitraj.py | 17 ++++++++--------- qutip/solver/nm_mcsolve.py | 2 +- 4 files changed, 11 insertions(+), 13 deletions(-) create mode 100644 doc/changes/2437.feature diff --git a/doc/changes/2437.feature b/doc/changes/2437.feature new file mode 100644 index 0000000000..4e1052b30c --- /dev/null +++ b/doc/changes/2437.feature @@ -0,0 +1 @@ +Allow mixed initial conditions for mcsolve and nm_mcsolve. \ No newline at end of file diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 2e742c8cad..3105b78cfb 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -537,8 +537,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): # Our best option is to return a trajectory result containing only # zeroes. This also ensures # that the final multi-trajectory # result will contain the requested number of trajectories. - state_qobj = self._restore_state(state, copy=True) - zero = qzero_like(state_qobj) + zero = qzero_like(self._restore_state(state, copy=True)) result = self._trajectory_resultclass(e_ops, self.options) result.collapse = [] for t in tlist: @@ -908,7 +907,6 @@ def StateFeedback( return _QobjFeedback(default, open=open) - class _unpack_arguments: """ If `f = _unpack_arguments(func, ('a', 'b'))` diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index c90f04a254..92f8591691 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -257,14 +257,6 @@ def run( result.stats['run time'] = time() - start_time return result - def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): - """ - Run one trajectory and return the result. - """ - result = self._initialize_run_one_traj(seed, state, tlist, e_ops, - **integrator_kwargs) - return self._integrate_one_traj(seed, tlist, result) - def _initialize_run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): result = self._trajectory_resultclass(e_ops, self.options) @@ -277,6 +269,14 @@ def _initialize_run_one_traj(self, seed, state, tlist, e_ops, result.add(tlist[0], self._restore_state(state, copy=False)) return result + def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): + """ + Run one trajectory and return the result. + """ + result = self._initialize_run_one_traj(seed, state, tlist, e_ops, + **integrator_kwargs) + return self._integrate_one_traj(seed, tlist, result) + def _integrate_one_traj(self, seed, tlist, result): for t, state in self._integrator.run(tlist): result.add(t, self._restore_state(state, copy=False)) @@ -424,7 +424,6 @@ def _get_generator(self, seed): return generator - class _InitialConditions: """ Information about mixed initial conditions, and the number of trajectories diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 41687815f4..74c464d2f2 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -553,7 +553,7 @@ def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): self._martingale.reset() return result - + def run_mixed(self, initial_conditions, tlist, ntraj, *, args=None, **kwargs): # update `args` dictionary before precomputing martingale From 6ac4b939c6bb117558dd1cc1a66fc0eb058e1061 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 29 May 2024 17:46:39 +0900 Subject: [PATCH 206/305] Minor documentation updates --- README.md | 1 + doc/conf.py | 4 +++- doc/frontmatter.rst | 4 ++++ doc/index.rst | 6 ++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9601a747be..08befc4d66 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ QuTiP: Quantum Toolbox in Python [J. Lishman](https://github.com/jakelishman), [S. Cross](https://github.com/hodgestar), [A. Galicia](https://github.com/AGaliciaMartinez), +[P. Menczel](https://github.com/pmenczel), [P. D. Nation](https://github.com/nonhermitian), and [J. R. Johansson](https://github.com/jrjohansson) diff --git a/doc/conf.py b/doc/conf.py index 71696eeb94..03cdb5783a 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -62,10 +62,12 @@ 'B. Li', 'J. Lishman', 'S. Cross', + 'A. Galicia', + 'P. Menczel', 'and E. Giguère' ]) -copyright = '2011 to 2021 inclusive, QuTiP developers and contributors' +copyright = '2011 to 2024 inclusive, QuTiP developers and contributors' def _check_source_folder_and_imported_qutip_match(): diff --git a/doc/frontmatter.rst b/doc/frontmatter.rst index ba45c750a1..cc7c11eb7f 100644 --- a/doc/frontmatter.rst +++ b/doc/frontmatter.rst @@ -40,6 +40,10 @@ This document contains a user guide and automatically generated API documentatio :Author: Simon Cross +:Author: Asier Galicia + +:Author: Paul Menczel + :release: |release| :copyright: diff --git a/doc/index.rst b/doc/index.rst index e49d7f6208..2e01d21075 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,6 +6,12 @@ QuTiP: Quantum Toolbox in Python ================================ + +This documentation contains a user guide and automatically generated API documentation for QuTiP. +For more information see the `QuTiP project web page `_. +Here, you can also find a collection of `tutorials for QuTiP `_. + + .. toctree:: :maxdepth: 3 From e85cfc804f987c538110778958908c191ff111dc Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 29 May 2024 19:18:13 +0900 Subject: [PATCH 207/305] Combined run_mixed and run into a single run method --- qutip/solver/mcsolve.py | 219 +++++++++++++++----------- qutip/solver/multitraj.py | 39 +---- qutip/solver/nm_mcsolve.py | 20 +-- qutip/tests/solver/test_mcsolve.py | 2 +- qutip/tests/solver/test_nm_mcsolve.py | 2 +- 5 files changed, 130 insertions(+), 152 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 3105b78cfb..7c788c3ee2 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -182,15 +182,8 @@ def mcsolve( ) mc = MCSolver(H, c_ops, options=options) - if state.isket: - result = mc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - seeds=seeds, target_tol=target_tol, timeout=timeout) - else: - if target_tol is not None: - warnings.warn('mcsolve does not support target tolerance ' - 'for mixed initial conditions') - result = mc.run_mixed(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - timeout=timeout, seeds=seeds) + result = mc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, + seeds=seeds, target_tol=target_tol, timeout=timeout) return result @@ -535,9 +528,9 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): # This can happen when a user uses "improved sampling" with a dark # initial state, or a mixed initial state containing a dark state. # Our best option is to return a trajectory result containing only - # zeroes. This also ensures # that the final multi-trajectory + # zeroes. This also ensures that the final multi-trajectory # result will contain the requested number of trajectories. - zero = qzero_like(self._restore_state(state, copy=True)) + zero = qzero_like(self._restore_state(state, copy=False)) result = self._trajectory_resultclass(e_ops, self.options) result.collapse = [] for t in tlist: @@ -553,79 +546,34 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): def run( self, - state: Qobj, + state: Qobj | list[tuple[Qobj, float]], tlist: ArrayLike, - ntraj: int = 1, + ntraj: int | list[int] = None, *, args: dict[str, Any] = None, e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, target_tol: float = None, timeout: float = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, - ) -> McResult: - # Overridden to sample the no-jump trajectory first. Then, the no-jump - # probability is used as a lower-bound for random numbers in future - # monte carlo runs - if not self.options["improved_sampling"]: - return super().run(state, tlist, ntraj=ntraj, args=args, - e_ops=e_ops, timeout=timeout, - target_tol=target_tol, seeds=seeds) - - seeds, result, map_func, map_kw, state0 = self._initialize_run( - state, ntraj, args=args, e_ops=e_ops, - timeout=timeout, target_tol=target_tol, seeds=seeds - ) - - # first run the no-jump trajectory - start_time = time() - seed0, no_jump_traj, no_jump_prob = ( - self._no_jump_simulation(state0, tlist, e_ops, seeds[0])) - result.add((seed0, no_jump_traj)) - result.stats['no jump run time'] = time() - start_time - - # run the remaining trajectories with the random number floor - # set to the no jump probability such that we only sample - # trajectories with jumps - start_time = time() - map_func( - self._run_one_traj, seeds[1:], - task_args=(state0, tlist, e_ops), - task_kwargs={'no_jump': False, 'jump_prob_floor': no_jump_prob}, - reduce_func=result.add, map_kw=map_kw, - progress_bar=self.options["progress_bar"], - progress_bar_kwargs=self.options["progress_kwargs"] - ) - result.stats['run time'] = time() - start_time - return result - - def run_mixed( - self, - initial_conditions: Qobj | list[tuple[Qobj, float]], - tlist: ArrayLike, - ntraj: int | list[int], - *, - args: dict[str, Any] = None, - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, - timeout: float = None, - seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> McResult: """ - Do the evolution of the Quantum system with a mixed initial state. + Do the evolution of the Quantum system. - The evolution is done as directed by ``rhs``. For each time in - ``tlist``, stores the state and/or expectation values in a - :class:`.MultiTrajResult`. The evolution method and stored results are - determined by ``options``. + For a ``state`` at time ``tlist[0]`` do the evolution as directed by + ``rhs`` and for each time in ``tlist`` store the state and/or + expectation values in a :class:`.MultiTrajResult`. The evolution method + and stored results are determined by ``options``. Parameters ---------- - initial_conditions : {:obj:`.Qobj`, list of (:obj:`.Qobj`, float)} - Statistical ensemble at the beginning of the evolution. May be - provided either as a density matrix, or as a list of tuples. In the - latter case, the first element of each tuple is a pure state, and - the second element is its weight, i.e., a number between 0 and 1 - describing the fraction of the ensemble in that state. The sum of - all weights is assumed to be one. + state : {:obj:`.Qobj`, list of (:obj:`.Qobj`, float)} + Initial state of the evolution. May be either a pure state or a + statistical ensemble. An ensemble can be provided either as a + density matrix, or as a list of tuples. In the latter case, the + first element of each tuple is a pure state, and the second element + is its weight, i.e., a number between 0 and 1 describing the + fraction of the ensemble in that state. The sum of all weights must + be one. tlist : list of double Time for which to save the results (state and/or expect) of the @@ -634,13 +582,14 @@ def run_mixed( not need to be uniformly distributed. ntraj : {int, list of int} - Number of trajectories to add. If a single number is provided, this - will be the total number of trajectories, which are distributed - over the initial ensemble automatically. - If `inditial_conditions` was specified as a list of pure states, - this parameter may also be a list of numbers with the same number - of entries, specifying manually the number of trajectories for each - pure state. + Number of trajectories to add. If the initial state is pure, this + must be single number. If the initial state is a mixed ensemble, + specified as a list of pure states, this parameter may also be a + list of numbers with the same number of entries. It then specifies + the number of trajectories for each pure state. If the initial + state is mixed and this parameter is a single number, it specifies + the total number of trajectories, which are distributed over the + initial ensemble automatically. args : dict, optional Change the ``args`` of the rhs for the evolution. @@ -653,8 +602,18 @@ def run_mixed( timeout : float, optional Maximum time in seconds for the trajectories to run. Once this time is reached, the simulation will end even if the number - of trajectories is less than ``ntraj``. In this case, the results - returned by this function will generally be invalid. + of trajectories is less than ``ntraj``. The map function, set in + options, can interupt the running trajectory or wait for it to + finish. Set to an arbitrary high number to disable. + + target_tol : {float, tuple, list}, optional + Target tolerance of the evolution. The evolution will compute + trajectories until the error on the expectation values is lower + than this tolerance. The maximum number of trajectories employed is + given by ``ntraj``. The error is computed using jackknife + resampling. ``target_tol`` can be an absolute tolerance or a pair + of absolute and relative tolerance, in that order. Lastly, it can + be a list of pairs of (atol, rtol) for each e_ops. seeds : {int, SeedSequence, list}, optional Seed or list of seeds for each trajectories. @@ -667,27 +626,95 @@ def run_mixed( .. note: The simulation will end when the first end condition is reached - between ``ntraj`` and ``timeout``. Setting a target tolerance is - not supported with mixed initial conditions. + between ``ntraj``, ``timeout`` and ``target_tol``. If the initial + condition is mixed, ``target_tol`` is not supported. If the initial + condition is mixed, and the end condition is not ``ntraj``, the + results returned by this functions will generally be invalid. """ - if isinstance(initial_conditions, Qobj): - # Decompose initial density matrix into eigenstates and eigenvalues - # In this case, we do not allow `ntraj` to be a list, since the - # order of the eigenstates is undefined + # We process the arguments and pass on to other functions depending on + # whether "improved sampling" is turned on, and whether the initial + # state is mixed. + if isinstance(state, (list, tuple)): + is_mixed = True + elif isinstance(state, Qobj): if isinstance(ntraj, (list, tuple)): - raise ValueError( - 'The `ntraj` parameter cannot be a list if the initial ' - 'conditions are given in the form of a density matrix') - eigenvalues, eigenstates = initial_conditions.eigenstates() - initial_conditions = [(state, weight) for state, weight - in zip(eigenstates, eigenvalues) - if weight > 0] + raise ValueError('The ntraj parameter can only be a list if ' + 'the initial conditions are mixed and given ' + 'in the form of a list of pure states') + is_mixed = not state.isket + if is_mixed: + # Mixed state given as density matrix. Decompose into list + # format, i.e., into eigenstates and eigenvalues + eigenvalues, eigenstates = state.eigenstates() + state = [(psi, p) for psi, p + in zip(eigenstates, eigenvalues) if p > 0] + + if is_mixed and target_tol is not None: + warnings.warn('Monte Carlo simulations with mixed initial ' + 'do not support target tolerance') + + # Default value for ntraj: as small as possible + # (2 per init. state for improved sampling, 1 per state otherwise) + if ntraj is None: + if is_mixed: + ntraj = len(state) + else: + ntraj = 1 + if self.options["improved_sampling"]: + ntraj *= 2 if not self.options["improved_sampling"]: - return super().run_mixed(initial_conditions, tlist, ntraj=ntraj, - args=args, e_ops=e_ops, timeout=timeout, - seeds=seeds) + if is_mixed: + return super()._run_mixed( + state, tlist, ntraj, args=args, e_ops=e_ops, + timeout=timeout, seeds=seeds) + else: + return super().run( + state, tlist, ntraj, args=args, e_ops=e_ops, + target_tol=target_tol, timeout=timeout, seeds=seeds) + if is_mixed: + return self._run_improved_sampling_mixed( + state, tlist, ntraj, args=args, e_ops=e_ops, + timeout=timeout, seeds=seeds) + return self._run_improved_sampling( + state, tlist, ntraj, args=args, e_ops=e_ops, + target_tol=target_tol, timeout=timeout, seeds=seeds) + + def _run_improved_sampling( + self, state, tlist, ntraj, *, + args, e_ops, target_tol, timeout, seeds): + # Sample the no-jump trajectory first. Then, the no-jump probability + # is used as a lower-bound for random numbers in future MC runs + seeds, result, map_func, map_kw, state0 = self._initialize_run( + state, ntraj, args=args, e_ops=e_ops, + timeout=timeout, target_tol=target_tol, seeds=seeds + ) + + # first run the no-jump trajectory + start_time = time() + seed0, no_jump_traj, no_jump_prob = ( + self._no_jump_simulation(state0, tlist, e_ops, seeds[0])) + result.add((seed0, no_jump_traj)) + result.stats['no jump run time'] = time() - start_time + + # run the remaining trajectories with the random number floor + # set to the no jump probability such that we only sample + # trajectories with jumps + start_time = time() + map_func( + self._run_one_traj, seeds[1:], + task_args=(state0, tlist, e_ops), + task_kwargs={'no_jump': False, 'jump_prob_floor': no_jump_prob}, + reduce_func=result.add, map_kw=map_kw, + progress_bar=self.options["progress_bar"], + progress_bar_kwargs=self.options["progress_kwargs"] + ) + result.stats['run time'] = time() - start_time + return result + def _run_improved_sampling_mixed( + self, initial_conditions, tlist, ntraj, *, + args, e_ops, timeout, seeds): seeds, result, map_func, map_kw, prepared_ics = self._initialize_run( initial_conditions, np.sum(ntraj), args=args, e_ops=e_ops, timeout=timeout, seeds=seeds) diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 92f8591691..897552f942 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -298,7 +298,7 @@ def _run_one_traj_mixed(self, id, seeds, ics, result.add_relative_weight(weight) return seed, result - def run_mixed( + def _run_mixed( self, initial_conditions: list[tuple[Qobj, float]], tlist: ArrayLike, @@ -310,12 +310,8 @@ def run_mixed( seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> MultiTrajResult: """ - Do the evolution of the Quantum system with a mixed initial state. - - The evolution is done as directed by ``rhs``. For each time in - ``tlist``, stores the state and/or expectation values in a - :class:`.MultiTrajResult`. The evolution method and stored results are - determined by ``options``. + Subclasses can use this method to allow simulations with a mixed + initial state. The following parameters differ from the `run` method: Parameters ---------- @@ -326,12 +322,6 @@ def run_mixed( describing the fraction of the ensemble in that state. The sum of all weights is assumed to be one. - tlist : list of double - Time for which to save the results (state and/or expect) of the - evolution. The first element of the list is the initial time of the - evolution. Time in the list must be in increasing order, but does - not need to be uniformly distributed. - ntraj : {int, list of int} Number of trajectories to add. If a single number is provided, this will be the total number of trajectories, which are distributed @@ -340,29 +330,6 @@ def run_mixed( `initial_conditions`, specifying the number of trajectories for each initial state explicitly. - args : dict, optional - Change the ``args`` of the rhs for the evolution. - - e_ops : list - list of Qobj or QobjEvo to compute the expectation values. - Alternatively, function[s] with the signature f(t, state) -> expect - can be used. - - timeout : float, optional - Maximum time in seconds for the trajectories to run. Once this time - is reached, the simulation will end even if the number - of trajectories is less than ``ntraj``. In this case, the results - returned by this function will generally be invalid. - - seeds : {int, SeedSequence, list}, optional - Seed or list of seeds for each trajectories. - - Returns - ------- - results : :class:`.MultiTrajResult` - Results of the evolution. States and/or expect will be saved. You - can control the saved data in the options. - .. note: The simulation will end when the first end condition is reached between ``ntraj`` and ``timeout``. Setting a target tolerance is diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 74c464d2f2..ae6c853b94 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -185,13 +185,8 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, ] nmmc = NonMarkovianMCSolver(H, ops_and_rates, options=options) - - if state.isket: - result = nmmc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - seeds=seeds, target_tol=target_tol, timeout=timeout) - else: - result = nmmc.run_mixed(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, - timeout=timeout, seeds=seeds) + result = nmmc.run(state, tlist=tlist, ntraj=ntraj, e_ops=e_ops, + seeds=seeds, target_tol=target_tol, timeout=timeout) return result @@ -554,17 +549,6 @@ def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): return result - def run_mixed(self, initial_conditions, tlist, ntraj, *, - args=None, **kwargs): - # update `args` dictionary before precomputing martingale - self._argument(args) - - self._martingale.initialize(tlist[0], cache=tlist) - result = super().run_mixed(initial_conditions, tlist, ntraj, **kwargs) - self._martingale.reset() - - return result - @property def options(self): """ diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index c2ccb0e53e..6f85c33b05 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -584,7 +584,7 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): solver = qutip.MCSolver( H, [L], options={'improved_sampling': improved_sampling}) - result = solver.run_mixed(initial_state, tlist, ntraj) + result = solver.run(initial_state, tlist, ntraj) if isinstance(initial_state, qutip.Qobj): reference = initial_state diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index de43d8bb6a..dfab3a99f7 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -718,7 +718,7 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): solver = qutip.NonMarkovianMCSolver( H, [(L, rate)], options={'improved_sampling': improved_sampling}) - result = solver.run_mixed(initial_state, tlist, ntraj) + result = solver.run(initial_state, tlist, ntraj) if isinstance(initial_state, qutip.Qobj): reference = initial_state From 6fe89617a6bb075d2bbbf8f1e5ab78429f8f954e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 29 May 2024 19:33:50 +0900 Subject: [PATCH 208/305] Updated list of admin team in docs and about --- README.md | 1 + doc/frontmatter.rst | 2 ++ qutip/about.py | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 08befc4d66..98f1b2790b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ QuTiP: Quantum Toolbox in Python [S. Cross](https://github.com/hodgestar), [A. Galicia](https://github.com/AGaliciaMartinez), [P. Menczel](https://github.com/pmenczel), +[P. Hopf](https://github.com/flowerthrower/), [P. D. Nation](https://github.com/nonhermitian), and [J. R. Johansson](https://github.com/jrjohansson) diff --git a/doc/frontmatter.rst b/doc/frontmatter.rst index cc7c11eb7f..ef5a35dbce 100644 --- a/doc/frontmatter.rst +++ b/doc/frontmatter.rst @@ -44,6 +44,8 @@ This document contains a user guide and automatically generated API documentatio :Author: Paul Menczel +:Author: Patrick Hopf + :release: |release| :copyright: diff --git a/qutip/about.py b/qutip/about.py index 0e5eb1662a..918ed77768 100644 --- a/qutip/about.py +++ b/qutip/about.py @@ -25,7 +25,8 @@ def about(): print( "Current admin team: Alexander Pitchford, " "Nathan Shammah, Shahnawaz Ahmed, Neill Lambert, Eric Giguère, " - "Boxi Li, Jake Lishman, Simon Cross and Asier Galicia." + "Boxi Li, Jake Lishman, Simon Cross, Asier Galicia, Paul Menczel, " + "and Patrick Hopf." ) print( "Board members: Daniel Burgarth, Robert Johansson, Anton F. Kockum, " From bb554d0ecb8d299e3fd859ba2218c82d62ef58ed Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 29 May 2024 19:35:29 +0900 Subject: [PATCH 209/305] Updated list of admin team in docs and about --- doc/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/conf.py b/doc/conf.py index 03cdb5783a..7fc7ef7fa7 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -64,6 +64,7 @@ 'S. Cross', 'A. Galicia', 'P. Menczel', + 'P. Hopf', 'and E. Giguère' ]) From d208d1d44abe4d23b0906e69a60e62af1ee5aa1b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 30 May 2024 16:49:19 +0900 Subject: [PATCH 210/305] Improved comments, added test --- qutip/solver/mcsolve.py | 16 +++++++---- qutip/tests/solver/test_mcsolve.py | 37 +++++++++++++++++++++++++ qutip/tests/solver/test_nm_mcsolve.py | 40 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 6 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 7c788c3ee2..81d33110de 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -156,7 +156,10 @@ def mcsolve( Notes ----- The simulation will end when the first end condition is reached between - ``ntraj``, ``timeout`` and ``target_tol``. + ``ntraj``, ``timeout`` and ``target_tol``. If the initial condition is + mixed, ``target_tol`` is not supported. If the initial condition is mixed, + and the end condition is not ``ntraj``, the results returned by this + function should be considered invalid. """ options = _solver_deprecation(kwargs, options, "mc") H = QobjEvo(H, args=args, tlist=tlist) @@ -503,8 +506,8 @@ def _initialize_stats(self): def _no_jump_simulation(self, state, tlist, e_ops, seed=None): """ Simulates the no-jump trajectory from the initial state `state0`. - Returns a tuple of the `TrajectoryResult` describing this trajectory, - and its probability. + Returns a tuple containing the seed, the `TrajectoryResult` describing + this trajectory, and the trajectory's probability. Note that a seed for the integrator may be provided, but is expected to be ignored in the no-jump simulation. """ @@ -629,14 +632,14 @@ def run( between ``ntraj``, ``timeout`` and ``target_tol``. If the initial condition is mixed, ``target_tol`` is not supported. If the initial condition is mixed, and the end condition is not ``ntraj``, the - results returned by this functions will generally be invalid. + results returned by this function should be considered invalid. """ # We process the arguments and pass on to other functions depending on # whether "improved sampling" is turned on, and whether the initial # state is mixed. if isinstance(state, (list, tuple)): is_mixed = True - elif isinstance(state, Qobj): + else: # state is Qobj, either pure state or dm if isinstance(ntraj, (list, tuple)): raise ValueError('The ntraj parameter can only be a list if ' 'the initial conditions are mixed and given ' @@ -651,7 +654,7 @@ def run( if is_mixed and target_tol is not None: warnings.warn('Monte Carlo simulations with mixed initial ' - 'do not support target tolerance') + 'state do not support target tolerance') # Default value for ntraj: as small as possible # (2 per init. state for improved sampling, 1 per state otherwise) @@ -751,6 +754,7 @@ def _run_improved_sampling_mixed( if None in no_jump_results: # timeout reached return result + # Process results of no-traj runs no_jump_probs = [] for (seed, res, prob), (_, weight) in ( zip(no_jump_results, prepared_ics)): diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index 6f85c33b05..dca0bdef88 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -593,3 +593,40 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): assert result.states[0] == reference assert result.num_trajectories == np.sum(ntraj) + + +@pytest.mark.parametrize("improved_sampling", [True, False]) +@pytest.mark.parametrize("p", [0, 0.25, 0.5]) +def test_mixed_equals_merged(improved_sampling, p): + # Running mcsolve with mixed ICs should be the same as running mcsolve + # multiple times and merging the results afterwards + initial_state1 = qutip.basis(2, 1) + initial_state2 = (qutip.basis(2, 1) + qutip.basis(2, 0)).unit() + H = qutip.sigmax() + L = qutip.sigmam() + tlist = [0, 1, 2] + ntraj = [3, 9] + + solver = qutip.MCSolver( + H, [L], options={'improved_sampling': improved_sampling}) + mixed_result = solver.run( + [(initial_state1, p), (initial_state2, 1 - p)], tlist, ntraj) + + # Reuse seeds, then results should be identical + seeds = mixed_result.seeds + if improved_sampling: + # For improved sampling, first two seeds are no-jump trajectories + seeds1 = seeds[0:1] + seeds[2:(ntraj[0]+1)] + seeds2 = seeds[1:2] + seeds[(ntraj[0]+1):] + else: + seeds1 = seeds[:ntraj[0]] + seeds2 = seeds[ntraj[0]:] + + pure_result1 = solver.run(initial_state1, tlist, ntraj[0], seeds=seeds1) + pure_result2 = solver.run(initial_state2, tlist, ntraj[1], seeds=seeds2) + merged_result = pure_result1.merge(pure_result2, p) + + assert mixed_result.num_trajectories == sum(ntraj) + assert merged_result.num_trajectories == sum(ntraj) + for state1, state2 in zip(mixed_result.states, merged_result.states): + assert state1 == state2 diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index dfab3a99f7..9f656dd7ce 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -727,3 +727,43 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): assert result.states[0] == reference assert result.num_trajectories == np.sum(ntraj) + + +@pytest.mark.parametrize("improved_sampling", [True, False]) +@pytest.mark.parametrize("p", [0, 0.25, 0.5]) +def test_mixed_equals_merged(improved_sampling, p): + # Running mcsolve with mixed ICs should be the same as running mcsolve + # multiple times and merging the results afterwards + initial_state1 = qutip.basis(2, 1) + initial_state2 = (qutip.basis(2, 1) + qutip.basis(2, 0)).unit() + H = qutip.sigmax() + L = qutip.sigmam() + def rate_function(t): + return -1 + t + tlist = [0, 1, 2] + ntraj = [3, 9] + + solver = qutip.NonMarkovianMCSolver( + H, [(L, rate_function)], + options={'improved_sampling': improved_sampling}) + mixed_result = solver.run( + [(initial_state1, p), (initial_state2, 1 - p)], tlist, ntraj) + + # Reuse seeds, then results should be identical + seeds = mixed_result.seeds + if improved_sampling: + # For improved sampling, first two seeds are no-jump trajectories + seeds1 = seeds[0:1] + seeds[2:(ntraj[0]+1)] + seeds2 = seeds[1:2] + seeds[(ntraj[0]+1):] + else: + seeds1 = seeds[:ntraj[0]] + seeds2 = seeds[ntraj[0]:] + + pure_result1 = solver.run(initial_state1, tlist, ntraj[0], seeds=seeds1) + pure_result2 = solver.run(initial_state2, tlist, ntraj[1], seeds=seeds2) + merged_result = pure_result1.merge(pure_result2, p) + + assert mixed_result.num_trajectories == sum(ntraj) + assert merged_result.num_trajectories == sum(ntraj) + for state1, state2 in zip(mixed_result.states, merged_result.states): + assert state1 == state2 From 351e5fa52a0e1609ebd4677bdca86ad9734a134b Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Mon, 3 Jun 2024 13:50:04 +0900 Subject: [PATCH 211/305] Improved test reliability --- qutip/tests/solver/test_mcsolve.py | 2 +- qutip/tests/solver/test_nm_mcsolve.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index dca0bdef88..a2cf8f1059 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -604,7 +604,7 @@ def test_mixed_equals_merged(improved_sampling, p): initial_state2 = (qutip.basis(2, 1) + qutip.basis(2, 0)).unit() H = qutip.sigmax() L = qutip.sigmam() - tlist = [0, 1, 2] + tlist = np.linspace(0, 2, 20) ntraj = [3, 9] solver = qutip.MCSolver( diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index 9f656dd7ce..eac5727339 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -740,7 +740,7 @@ def test_mixed_equals_merged(improved_sampling, p): L = qutip.sigmam() def rate_function(t): return -1 + t - tlist = [0, 1, 2] + tlist = np.linspace(0, 2, 20) ntraj = [3, 9] solver = qutip.NonMarkovianMCSolver( From 01eacaadf200a6c7e2742381b5b699f2987e3c59 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 3 Jun 2024 16:36:14 -0400 Subject: [PATCH 212/305] Fix steadystate permutation being inversed --- qutip/solver/steadystate.py | 6 ++-- qutip/tests/solver/test_steadystate.py | 42 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py index 7f1424f7f6..9e2d0e6ecb 100644 --- a/qutip/solver/steadystate.py +++ b/qutip/solver/steadystate.py @@ -12,14 +12,16 @@ def _permute_wbm(L, b): - perm = scipy.sparse.csgraph.maximum_bipartite_matching(L.as_scipy()) + perm = np.argsort( + scipy.sparse.csgraph.maximum_bipartite_matching(L.as_scipy()) + ) L = _data.permute.indices(L, perm, None) b = _data.permute.indices(b, perm, None) return L, b def _permute_rcm(L, b): - perm = scipy.sparse.csgraph.reverse_cuthill_mckee(L.as_scipy()) + perm = np.argsort(scipy.sparse.csgraph.reverse_cuthill_mckee(L.as_scipy())) L = _data.permute.indices(L, perm, perm) b = _data.permute.indices(b, perm, None) return L, b, perm diff --git a/qutip/tests/solver/test_steadystate.py b/qutip/tests/solver/test_steadystate.py index df87f4f440..43810caa44 100644 --- a/qutip/tests/solver/test_steadystate.py +++ b/qutip/tests/solver/test_steadystate.py @@ -4,6 +4,8 @@ import qutip import warnings from packaging import version as pac_version +from qutip.solver.steadystate import _permute_rcm, _permute_wbm +import qutip.core.data as _data @pytest.mark.parametrize(['method', 'kwargs'], [ @@ -231,6 +233,46 @@ def test_steadystate_floquet(sparse): assert rho_ss.tr() == pytest.approx(1, abs=1e-15) +def test_rcm(): + N = 5 + a = qutip.destroy(N, dtype="CSR") + I = qutip.qeye(N, dtype="CSR") + H = (a + a.dag() & I) + (I & a * a.dag()) + c_ops = [a & I, I & a] + L = qutip.liouvillian(H, c_ops).data + b = qutip.basis(N**4).data + + def bandwidth(mat): + return sum(scipy.linalg.bandwidth(mat.to_array())) + + # rcm should reduce bandwidth + assert bandwidth(L) > bandwidth(_permute_rcm(L, b)[0]) + + +def test_wbm(): + N = 5 + a = qutip.destroy(N, dtype="CSR") + I = qutip.qeye(N, dtype="CSR") + H = (a + a.dag() & I) + (I & a * a.dag()) + c_ops = [a & I, I & a] + L = qutip.liouvillian(H, c_ops).data + b = qutip.basis(N**4).data + + # shuffling the Liouvillian to ensure the diag is almost empty + perm = np.arange(N**4) + np.random.shuffle(perm) + L = _data.permute.indices(L, None, perm) + + def dia_dominance(mat): + mat = mat.to_array() + norm = np.sum(np.abs(mat)) + diag = np.sum(np.abs(np.diagonal(mat))) + return diag / norm + + # wbm increase diagonal dominance + assert dia_dominance(L) < dia_dominance(_permute_wbm(L, b)[0]) + + def test_bad_options_steadystate(): N = 4 a = qutip.destroy(N) From 9c5890c9214590ff4b9f8ecdf90e34162711cdb4 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 3 Jun 2024 16:38:05 -0400 Subject: [PATCH 213/305] Add towncrier --- doc/changes/2443.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2443.bugfix diff --git a/doc/changes/2443.bugfix b/doc/changes/2443.bugfix new file mode 100644 index 0000000000..5e08a0dd71 --- /dev/null +++ b/doc/changes/2443.bugfix @@ -0,0 +1 @@ +Fix steadystate permutation being reversed. From a62f3d0ce03c260ef0ca54b87173df9a814fb034 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 4 Jun 2024 10:46:29 -0400 Subject: [PATCH 214/305] Ensure prepare_state support jax --- qutip/solver/solver_base.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index 207c734dc3..30a8b29038 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -98,13 +98,23 @@ def _prepare_state(self, state): # anything other than dimensions. 'isherm': state.isherm and not (self.rhs.dims == state.dims) } - if state.isoper: + if state.isket: + norm = state.norm() + elif state._dims.issquare: + # Qobj.isoper does not differientiate between rectangular operators + # and normal ones. norm = state.tr() else: - norm = state.norm() - # Use the settings atol instead of the solver one since the second - # refer to the ODE tolerance and some integrator do not use it. - self._normalized = np.abs(norm - 1) <= settings.core["atol"] + norm = -1 + self._normalize_output = ( + self._options.get("normalize_output", False) + # Don't normalize output if input is not normalized. + # Use the settings atol instead of the solver one since the second + # refer to the ODE tolerance and some integrator do not use it. + and np.abs(norm - 1) <= settings.core["atol"] + # Only ket and dm can be normalized + and (self.rhs.dims[1] == state.dims or state.shape[1] == 1) + ) if self.rhs.dims[1] == state.dims: return stack_columns(state.data) return state.data @@ -119,11 +129,7 @@ def _restore_state(self, data, *, copy=True): else: state = Qobj(data, **self._state_metadata, copy=copy) - if ( - data.shape[1] == 1 - and self._options['normalize_output'] - and self._normalized - ): + if self._normalize_output: if state.isoper: state = state * (1 / state.tr()) else: From aebf137850b044e04d345ac211369016bde8658d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Tue, 4 Jun 2024 16:29:27 -0400 Subject: [PATCH 215/305] Apply suggestions from code review Co-authored-by: Pieter Eendebak --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c920904c90..3536744bad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: os: ubuntu-latest python-version: "3.12" scipy-requirement: "" - numpy-requirement: "==2.0.0rc1" + numpy-requirement: "==2.0.0rc2" pypi: 1 # Binaries compiled with numpy 2 should be compatible when using @@ -51,7 +51,7 @@ jobs: os: ubuntu-latest python-version: "3.10" scipy-requirement: "" - numpy-requirement: "==2.0.0rc1" + numpy-requirement: "==2.0.0rc2" roll_back_numpy: 1 pypi: 1 # numpy 2 not yet available on conda From 4c56a586516668f61038d75a069a255d2e377aa6 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 4 Jun 2024 16:47:07 -0400 Subject: [PATCH 216/305] Use less old version on numpy rollback since it's not supported by mpl 3.9 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1080dd1f0a..3366b0028f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -194,7 +194,7 @@ jobs: if [[ "${{ matrix.roll_back_numpy }}" ]]; then # Binary compiled with numpy 2.X should be compatible with numpy 1.X - python -m pip install "numpy<1.23" + python -m pip install "numpy<1.24" fi From 3de8df7b4ec4845a4ddb166ad65ae3bfae90906e Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 5 Jun 2024 14:52:12 +0900 Subject: [PATCH 217/305] Suggestions from code review --- qutip/solver/mcsolve.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 81d33110de..33b19fbc7c 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -178,6 +178,11 @@ def mcsolve( return mesolve(H, state, tlist, e_ops=e_ops, args=args, options=options) + if not isinstance(state, Qobj): + raise TypeError( + "The initial state for mcsolve must be a Qobj. Use the MCSolver " + "class for more options of specifying mixed initial states." + ) if isinstance(ntraj, (list, tuple)): raise TypeError( "ntraj must be an integer. " @@ -562,10 +567,10 @@ def run( """ Do the evolution of the Quantum system. - For a ``state`` at time ``tlist[0]`` do the evolution as directed by - ``rhs`` and for each time in ``tlist`` store the state and/or - expectation values in a :class:`.MultiTrajResult`. The evolution method - and stored results are determined by ``options``. + For a ``state`` at time ``tlist[0]``, do up to ``ntraj`` simulations of + the Monte-Carlo evolution. For each time in ``tlist`` store the state + and/or expectation values in a :class:`.MultiTrajResult`. The evolution + method and stored results are determined by ``options``. Parameters ---------- @@ -644,7 +649,7 @@ def run( raise ValueError('The ntraj parameter can only be a list if ' 'the initial conditions are mixed and given ' 'in the form of a list of pure states') - is_mixed = not state.isket + is_mixed = state.isoper and not self.rhs.issuper if is_mixed: # Mixed state given as density matrix. Decompose into list # format, i.e., into eigenstates and eigenvalues @@ -748,9 +753,7 @@ def _run_improved_sampling_mixed( no_jump_results = map_func( _unpack_arguments(self._no_jump_simulation, ('state', 'seed')), [(state, seed) for seed, (state, _) in zip(seeds, prepared_ics)], - task_kwargs={'tlist': tlist, 'e_ops': e_ops}, - map_kw=map_kw, progress_bar=self.options["progress_bar"], - progress_bar_kwargs=self.options["progress_kwargs"]) + task_kwargs={'tlist': tlist, 'e_ops': e_ops}, map_kw=map_kw) if None in no_jump_results: # timeout reached return result From 393c5462e4d7a87ea9197a5a5c9ffaaa4db61820 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Wed, 5 Jun 2024 15:25:12 +0900 Subject: [PATCH 218/305] Store ntraj per initial state in multi trajectory results --- qutip/solver/mcsolve.py | 12 ++++++++++-- qutip/solver/multitraj.py | 6 +++++- qutip/solver/nm_mcsolve.py | 4 +++- qutip/tests/solver/test_mcsolve.py | 18 ++++++++++++++++++ qutip/tests/solver/test_nm_mcsolve.py | 18 ++++++++++++++++++ 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 33b19fbc7c..3487558477 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -152,6 +152,8 @@ def mcsolve( Object storing all results from the simulation. Which results is saved depends on the presence of ``e_ops`` and the options used. ``collapse`` and ``photocurrent`` is available to Monte Carlo simulation results. + If the initial condition is mixed, the result has additional attributes + ``initial_states`` and ``ntraj_per_initial_state``. Notes ----- @@ -628,9 +630,11 @@ def run( Returns ------- - results : :class:`.MultiTrajResult` + results : :class:`.McResult` Results of the evolution. States and/or expect will be saved. You - can control the saved data in the options. + can control the saved data in the options. If the initial condition + is mixed, the result has additional attributes ``initial_states`` + and ``ntraj_per_initial_state``. .. note: The simulation will end when the first end condition is reached @@ -782,6 +786,10 @@ def _run_improved_sampling_mixed( progress_bar_kwargs=self.options["progress_kwargs"] ) result.stats['run time'] = time() - start_time + result.initial_states = [self._restore_state(state, copy=False) + for state, _ in ics_info.state_list] + # add back +1 for the no-jump trajectories: + result.ntraj_per_initial_state = [(n+1) for n in ics_info.ntraj] return result def _get_integrator(self): diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 897552f942..646e38ab45 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -338,15 +338,19 @@ def _run_mixed( seeds, result, map_func, map_kw, prepared_ics = self._initialize_run( initial_conditions, np.sum(ntraj), args=args, e_ops=e_ops, timeout=timeout, seeds=seeds) + ics_info = _InitialConditions(prepared_ics, ntraj) start_time = time() map_func( self._run_one_traj_mixed, range(len(seeds)), - (seeds, _InitialConditions(prepared_ics, ntraj), tlist, e_ops), + (seeds, ics_info, tlist, e_ops), reduce_func=result.add, map_kw=map_kw, progress_bar=self.options["progress_bar"], progress_bar_kwargs=self.options["progress_kwargs"] ) result.stats['run time'] = time() - start_time + result.initial_states = [self._restore_state(state, copy=False) + for state, _ in ics_info.state_list] + result.ntraj_per_initial_state = list(ics_info.ntraj) return result def _read_seed(self, seed, ntraj): diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index ae6c853b94..fb4677f835 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -163,7 +163,9 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, ``trace`` (and ``runs_trace`` if ``store_final_state`` is set). Note that the states on the individual trajectories are not normalized. This field contains the average of their trace, which will converge to one - in the limit of sufficiently many trajectories. + in the limit of sufficiently many trajectories. If the initial + condition is mixed, the result has additional attributes + ``initial_states`` and ``ntraj_per_initial_state``. """ H = QobjEvo(H, args=args, tlist=tlist) diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index a2cf8f1059..e106798ae3 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -594,6 +594,17 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): assert result.states[0] == reference assert result.num_trajectories == np.sum(ntraj) + assert hasattr(result, 'initial_states') + assert isinstance(result.initial_states, list) + assert all(isinstance(st, qutip.Qobj) for st in result.initial_states) + assert hasattr(result, 'ntraj_per_initial_state') + assert isinstance(result.ntraj_per_initial_state, list) + assert len(result.ntraj_per_initial_state) == len(result.initial_states) + if isinstance(ntraj, list): + assert result.ntraj_per_initial_state == ntraj + else: + assert sum(result.ntraj_per_initial_state) == ntraj + @pytest.mark.parametrize("improved_sampling", [True, False]) @pytest.mark.parametrize("p", [0, 0.25, 0.5]) @@ -630,3 +641,10 @@ def test_mixed_equals_merged(improved_sampling, p): assert merged_result.num_trajectories == sum(ntraj) for state1, state2 in zip(mixed_result.states, merged_result.states): assert state1 == state2 + + assert hasattr(mixed_result, 'initial_states') + assert isinstance(mixed_result.initial_states, list) + assert mixed_result.initial_states == [initial_state1, initial_state2] + assert hasattr(mixed_result, 'ntraj_per_initial_state') + assert isinstance(mixed_result.ntraj_per_initial_state, list) + assert mixed_result.ntraj_per_initial_state == ntraj diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index eac5727339..091bd7383b 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -728,6 +728,17 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): assert result.states[0] == reference assert result.num_trajectories == np.sum(ntraj) + assert hasattr(result, 'initial_states') + assert isinstance(result.initial_states, list) + assert all(isinstance(st, qutip.Qobj) for st in result.initial_states) + assert hasattr(result, 'ntraj_per_initial_state') + assert isinstance(result.ntraj_per_initial_state, list) + assert len(result.ntraj_per_initial_state) == len(result.initial_states) + if isinstance(ntraj, list): + assert result.ntraj_per_initial_state == ntraj + else: + assert sum(result.ntraj_per_initial_state) == ntraj + @pytest.mark.parametrize("improved_sampling", [True, False]) @pytest.mark.parametrize("p", [0, 0.25, 0.5]) @@ -767,3 +778,10 @@ def rate_function(t): assert merged_result.num_trajectories == sum(ntraj) for state1, state2 in zip(mixed_result.states, merged_result.states): assert state1 == state2 + + assert hasattr(mixed_result, 'initial_states') + assert isinstance(mixed_result.initial_states, list) + assert mixed_result.initial_states == [initial_state1, initial_state2] + assert hasattr(mixed_result, 'ntraj_per_initial_state') + assert isinstance(mixed_result.ntraj_per_initial_state, list) + assert mixed_result.ntraj_per_initial_state == ntraj From c06ee44427bf5e3aaab048a5484644761d244e26 Mon Sep 17 00:00:00 2001 From: Paul Menczel Date: Thu, 6 Jun 2024 13:40:30 +0900 Subject: [PATCH 219/305] Mixed mcsolve documentation --- doc/guide/dynamics/dynamics-monte.rst | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/doc/guide/dynamics/dynamics-monte.rst b/doc/guide/dynamics/dynamics-monte.rst index 0115b9cd6a..e5e94cb90c 100644 --- a/doc/guide/dynamics/dynamics-monte.rst +++ b/doc/guide/dynamics/dynamics-monte.rst @@ -282,6 +282,77 @@ trajectories: plt.show() +Mixed Initial states +-------------------- + +The Monte-Carlo solver can be used for mixed initial states. For example, if a +qubit can initially be in the excited state :math:`|+\rangle` with probability +:math:`p` or in the ground state :math:`|-\rangle` with probability +:math:`(1-p)`, the initial state is described by the density matrix +:math:`\rho_0 = p | + \rangle\langle + | + (1-p) | - \rangle\langle - |`. + +In QuTiP, this initial density matrix can be created as follows: + +.. code-block:: + + ground = qutip.basis(2, 0) + excited = qutip.basis(2, 1) + density_matrix = p * excited.proj() + (1 - p) * ground.proj() + +One can then pass this density matrix directly to ``mcsolve``, as in + +.. code-block:: + + mcsolve(H, density_matrix, ...) + +Alternatively, using the class interface, if ``solver`` is an +:class:`.MCSolver` object, one can either call +``solver.run(density_matrix, ...)`` or pass the list of initial states like + +.. code-block:: + + solver.run([(excited, p), (ground, 1-p)], ...) + +The number of trajectories can still be specified as a single number ``ntraj``. +In that case, QuTiP will automatically decide how many trajectories to use for +each of the initial states, guaranteeing that the total number of trajectories +is exactly the specified number. When using the class interface and providing +the initial state as a list, the `ntraj` parameter may also be a list +specifying the number of trajectories to use for each state manually. In either +case, the resulting :class:`McResult` will have attributes ``initial_states`` +and ``ntraj_per_initial_state`` listing the initial states and the +corresponding numbers of trajectories that were actually used. + +Note that in general, the fraction of trajectories starting in a given initial +state will (and can) not exactly match the probability :math:`p` of that state +in the initial ensemble. In this case, QuTiP will automatically apply a +correction to the averages, weighting for example the initial states with +"too few" trajectories more strongly. Therefore, the initial state returned in +the result object will always match the provided one up to numerical +inaccuracies. Furthermore, the result returned by the `mcsolve` call above is +equivalent to the following: + +.. code-block:: + + result1 = qutip.mcsolve(H, excited, ...) + result2 = qutip.mcsolve(H, ground, ...) + result1.merge(result2, p) + +However, the single ``mcsolve`` call allows for more parallelization (see +below). + +The Monte-Carlo solver with a mixed initial state currently does not support +specifying a target tolerance. Also, in case the simulation ends early due to +timeout, it is not guaranteed that all initial states have been sampled. If +not all initial states have been sampled, the resulting states will not be +normalized, and the result should be discarded. + +Finally note that what we just discussed concerns the case of mixed initial +states where the provided Hamiltonian is an operator. If it is a superoperator +(i.e., a Liouvillian), ``mcsolve`` will generate trajectories of mixed states +(see below) and the present discussion does not apply. + + Using the Improved Sampling Algorithm ------------------------------------- From c874c4a023d550cfb6b232ece6c930cd9b237719 Mon Sep 17 00:00:00 2001 From: Anush Venkatakrishnan <54374648+anushkrishnav@users.noreply.github.com> Date: Sat, 8 Jun 2024 12:21:39 +0530 Subject: [PATCH 220/305] Fixing rendering problem (#2442) Resolved a rendering issue in the process matrix visualization. Previously, the code did not utilize matplotlib's built-in z-sorting mechanism. Experiments with various z-sort configurations (min, max, average) yielded inconsistent results across different charts. The solution was inspired by a Stack Overflow discussion (https://stackoverflow.com/questions/18602660/matplotlib-bar3d-clipping-problems). By adjusting the calculation of camera coordinates and incorporating minor modifications from the suggested approach, the rendering issue has been successfully addressed. --- doc/changes/2400.bugfix | 3 + qutip/visualization.py | 196 ++++++++++++++++++++++++++++------------ 2 files changed, 140 insertions(+), 59 deletions(-) create mode 100644 doc/changes/2400.bugfix diff --git a/doc/changes/2400.bugfix b/doc/changes/2400.bugfix new file mode 100644 index 0000000000..9c850f9fb7 --- /dev/null +++ b/doc/changes/2400.bugfix @@ -0,0 +1,3 @@ +Bug Fix in Process Matrix Rendering + +Resolved a rendering issue in the process matrix visualization. Previously, the code did not utilize matplotlib's built-in z-sorting mechanism. Experiments with various z-sort configurations (min, max, average) yielded inconsistent results across different charts. The solution was inspired by a Stack Overflow discussion (https://stackoverflow.com/questions/18602660/matplotlib-bar3d-clipping-problems). By adjusting the calculation of camera coordinates and incorporating minor modifications from the suggested approach, the rendering issue has been successfully addressed. \ No newline at end of file diff --git a/qutip/visualization.py b/qutip/visualization.py index b5f10f7dcf..048c0b57da 100644 --- a/qutip/visualization.py +++ b/qutip/visualization.py @@ -9,7 +9,6 @@ 'plot_spin_distribution', 'complex_array_to_rgb', 'plot_qubism', 'plot_schmidt'] -import warnings import itertools as it import numpy as np from numpy import pi, array, sin, cos, angle, log2, sqrt @@ -17,9 +16,8 @@ from packaging.version import parse as parse_version from . import ( - Qobj, isket, ket2dm, tensor, vector_to_operator, to_super, settings + Qobj, isket, ket2dm, tensor, vector_to_operator, settings ) -from .core.dimensions import flatten from .core.superop_reps import _to_superpauli, isqubitdims from .wigner import wigner from .matplotlib_utilities import complex_phase_cmap @@ -670,10 +668,54 @@ def _get_matrix_components(option, M, argument): f"{option} for {argument}") -def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, - bar_style='real', color_limits=None, color_style='real', - options=None, *, cmap=None, colorbar=True, - fig=None, ax=None): +def sph2cart(r, theta, phi): + """spherical to cartesian transformation.""" + x = r * np.sin(theta) * np.cos(phi) + y = r * np.sin(theta) * np.sin(phi) + z = r * np.cos(theta) + return x, y, z + + +def sphview(ax): + """ + returns the camera position for 3D axes in spherical coordinates.""" + xlim = ax.get_xlim() + ylim = ax.get_ylim() + zlim = ax.get_zlim() + # Compute based on the plots xyz limits. + r = 0.5 * np.sqrt( + (xlim[1] - xlim[0]) ** 2 + + (ylim[1] - ylim[0]) ** 2 + + (zlim[1] - zlim[0]) ** 2 + ) + theta, phi = np.radians((90 - ax.elev, ax.azim)) + return r, theta, phi + + +def get_camera_position(ax): + """ + returns the camera position for 3D axes in cartesian coordinates + as a 3d numpy array. + """ + r, theta, phi = sphview(ax) + return np.array(sph2cart(r, theta, phi), ndmin=3).T + + +def matrix_histogram( + M, + x_basis=None, + y_basis=None, + limits=None, + bar_style="real", + color_limits=None, + color_style="real", + options=None, + *, + cmap=None, + colorbar=True, + fig=None, + ax=None, +): """ Draw a histogram for the matrix M, with the given x and y labels and title. @@ -791,11 +833,20 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, """ - # default options - default_opts = {'zticks': None, 'bars_spacing': 0.2, - 'bars_alpha': 1., 'bars_lw': 0.5, 'bars_edgecolor': 'k', - 'shade': True, 'azim': -35, 'elev': 35, 'stick': False, - 'cbar_pad': 0.04, 'cbar_to_z': False, 'threshold': None} + default_opts = { + "zticks": None, + "bars_spacing": 0.3, + "bars_alpha": 1.0, + "bars_lw": 0.7, + "bars_edgecolor": "k", + "shade": True, + "azim": -60, + "elev": 30, + "stick": False, + "cbar_pad": 0.04, + "cbar_to_z": False, + "threshold": None, + } # update default_opts from input options if options is None: @@ -804,8 +855,10 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, if isinstance(options, dict): # check if keys in options dict are valid if set(options) - set(default_opts): - raise ValueError("invalid key(s) found in options: " - f"{', '.join(set(options) - set(default_opts))}") + raise ValueError( + "invalid key(s) found in options: " + f"{', '.join(set(options) - set(default_opts))}" + ) else: # updating default options default_opts.update(options) @@ -813,7 +866,7 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, else: raise ValueError("options must be a dictionary") - fig, ax = _is_fig_and_ax(fig, ax, projection='3d') + fig, ax = _is_fig_and_ax(fig, ax, projection="3d") if not isinstance(M, list): Ms = [M] @@ -822,8 +875,7 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, _equal_shape(Ms) - for i in range(len(Ms)): - M = Ms[i] + for i, M in enumerate(Ms): if isinstance(M, Qobj): if x_basis is None: x_basis = list(_cb_labels([M.shape[0]])[0]) @@ -832,10 +884,9 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, # extract matrix data from Qobj M = M.full() - bar_M = _get_matrix_components(bar_style, M, 'bar_style') + bar_M = _get_matrix_components(bar_style, M, "bar_style") - if isinstance(limits, list) and \ - len(limits) == 2: + if isinstance(limits, list) and len(limits) == 2: z_min = limits[0] z_max = limits[1] else: @@ -846,19 +897,18 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, z_min -= 0.1 z_max += 0.1 - color_M = _get_matrix_components(color_style, M, 'color_style') + color_M = _get_matrix_components(color_style, M, "color_style") - if isinstance(color_limits, list) and \ - len(color_limits) == 2: + if isinstance(color_limits, list) and len(color_limits) == 2: c_min = color_limits[0] c_max = color_limits[1] else: - if color_style == 'phase': + if color_style == "phase": c_min = -pi c_max = pi else: c_min = min(color_M) if i == 0 else min(min(color_M), c_min) - c_max = min(color_M) if i == 0 else max(max(color_M), c_max) + c_max = max(color_M) if i == 0 else max(max(color_M), c_max) if c_min == c_max: c_min -= 0.1 @@ -868,66 +918,93 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, if cmap is None: # change later - if color_style == 'phase': + if color_style == "phase": cmap = _cyclic_cmap() else: cmap = _sequential_cmap() artist_list = list() + + ax.view_init(azim=options['azim'], elev=options['elev']) + + camera = get_camera_position(ax) for M in Ms: if isinstance(M, Qobj): M = M.full() - bar_M = _get_matrix_components(bar_style, M, 'bar_style') - color_M = _get_matrix_components(color_style, M, 'color_style') + bar_M = _get_matrix_components(bar_style, M, "bar_style") + color_M = _get_matrix_components(color_style, M, "color_style") n = np.size(M) xpos, ypos = np.meshgrid(range(M.shape[0]), range(M.shape[1])) xpos = xpos.T.flatten() + 0.5 ypos = ypos.T.flatten() + 0.5 zpos = np.zeros(n) - dx = dy = (1 - options['bars_spacing']) * np.ones(n) + dx = dy = (1 - options["bars_spacing"]) * np.ones(n) colors = cmap(norm(color_M)) - colors[:, 3] = options['bars_alpha'] + colors[:, 3] = options["bars_alpha"] - if options['threshold'] is not None: - colors[:, 3] *= 1 * (bar_M >= options['threshold']) + if options["threshold"] is not None: + colors[:, 3] *= 1 * (bar_M >= options["threshold"]) - idx, = np.where(bar_M < options['threshold']) + (idx,) = np.where(bar_M < options["threshold"]) bar_M[idx] = 0 - artist = ax.bar3d(xpos, ypos, zpos, dx, dy, bar_M, color=colors, - edgecolors=options['bars_edgecolor'], - linewidths=options['bars_lw'], - shade=options['shade']) - artist_list.append([artist]) + temp_xpos = xpos.reshape(M.shape) + temp_ypos = ypos.reshape(M.shape) + temp_zpos = zpos.reshape(M.shape) + + # calculating z_order for each bar based on its position + # The sorting issue was fixed by making minor change to + # https://stackoverflow.com/questions/18602660/matplotlib-bar3d-clipping-problems + z_order = ( + np.multiply( + [ + temp_xpos, temp_ypos, temp_zpos], camera + ).sum(0).flatten() + ) + + for i, uxpos in enumerate(xpos): + artist = ax.bar3d( + uxpos, + ypos[i], + zpos[i], + dx[i], + dy[i], + bar_M[i], + color=colors[i], + edgecolors=options["bars_edgecolor"], + linewidths=options["bars_lw"], + shade=options["shade"], + ) + # Setting the z-order for rendering + artist._sort_zpos = z_order[i] + artist_list.append([artist]) if len(Ms) == 1: output = ax else: - output = animation.ArtistAnimation(fig, artist_list, interval=50, - blit=True, repeat_delay=1000) + output = animation.ArtistAnimation( + fig, artist_list, interval=50, blit=True, repeat_delay=1000 + ) # remove vertical lines on xz and yz plane - ax.yaxis._axinfo["grid"]['linewidth'] = 0 - ax.xaxis._axinfo["grid"]['linewidth'] = 0 + ax.yaxis._axinfo["grid"]["linewidth"] = 0 + ax.xaxis._axinfo["grid"]["linewidth"] = 0 # x axis - _update_xaxis(options['bars_spacing'], M, ax, x_basis) + _update_xaxis(options["bars_spacing"], M, ax, x_basis) # y axis - _update_yaxis(options['bars_spacing'], M, ax, y_basis) + _update_yaxis(options["bars_spacing"], M, ax, y_basis) # z axis - _update_zaxis(ax, z_min, z_max, options['zticks']) + _update_zaxis(ax, z_min, z_max, options["zticks"]) # stick to xz and yz plane - _stick_to_planes(options['stick'], - options['azim'], ax, M, - options['bars_spacing']) - ax.view_init(azim=options['azim'], elev=options['elev']) + _stick_to_planes(options["stick"], options["azim"], ax, M, options["bars_spacing"]) # removing margins _remove_margins(ax.xaxis) @@ -936,22 +1013,23 @@ def matrix_histogram(M, x_basis=None, y_basis=None, limits=None, # color axis if colorbar: - cax, kw = mpl.colorbar.make_axes(ax, shrink=.75, - pad=options['cbar_pad']) + cax, kw = mpl.colorbar.make_axes( + ax, shrink=0.75, pad=options["cbar_pad"]) cb = mpl.colorbar.ColorbarBase(cax, cmap=cmap, norm=norm) - if color_style == 'real': - cb.set_label('real') - elif color_style == 'img': - cb.set_label('imaginary') - elif color_style == 'abs': - cb.set_label('absolute') + if color_style == "real": + cb.set_label("real") + elif color_style == "img": + cb.set_label("imaginary") + elif color_style == "abs": + cb.set_label("absolute") else: - cb.set_label('arg') + cb.set_label("arg") if color_limits is None: cb.set_ticks([-pi, -pi / 2, 0, pi / 2, pi]) cb.set_ticklabels( - (r'$-\pi$', r'$-\pi/2$', r'$0$', r'$\pi/2$', r'$\pi$')) + (r"$-\pi$", r"$-\pi/2$", r"$0$", r"$\pi/2$", r"$\pi$") + ) return fig, output From 167c30b60e77294b285fbc50c38e44e33c8ae5da Mon Sep 17 00:00:00 2001 From: alan-nala <44471696+alan-nala@users.noreply.github.com> Date: Wed, 12 Jun 2024 04:39:59 -0300 Subject: [PATCH 221/305] Gates in qutip-v5 User Guide (#2441) Co-authored-by: alan-nala --- doc/apidoc/functions.rst | 7 +++ doc/changes/2441.doc | 1 + doc/guide/guide-basics.rst | 1 + doc/guide/guide-states.rst | 96 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 doc/changes/2441.doc diff --git a/doc/apidoc/functions.rst b/doc/apidoc/functions.rst index f5d58db96a..475c69aa98 100644 --- a/doc/apidoc/functions.rst +++ b/doc/apidoc/functions.rst @@ -21,6 +21,13 @@ Quantum Operators :members: charge, commutator, create, destroy, displace, fcreate, fdestroy, jmat, num, qeye, identity, momentum, phase, position, qdiags, qutrit_ops, qzero, sigmam, sigmap, sigmax, sigmay, sigmaz, spin_Jx, spin_Jy, spin_Jz, spin_Jm, spin_Jp, squeeze, squeezing, tunneling, qeye_like, qzero_like +Quantum Gates +----------------- + +.. automodule:: qutip.core.gates + :members: rx, ry, rz, sqrtnot, snot, phasegate, qrot, cy_gate, cz_gate, s_gate, t_gate, cs_gate, ct_gate, cphase, cnot, csign, berkeley, swapalpha, swap, iswap, sqrtswap, sqrtiswap, fredkin, molmer_sorensen, toffoli, hadamard_transform, qubit_clifford_group, globalphase + + Energy Restricted Operators --------------------------- diff --git a/doc/changes/2441.doc b/doc/changes/2441.doc new file mode 100644 index 0000000000..981bf2660c --- /dev/null +++ b/doc/changes/2441.doc @@ -0,0 +1 @@ +Added `qutip.core.gates` to apidoc/functions.rst and a Gates section to guide-states.rst. diff --git a/doc/guide/guide-basics.rst b/doc/guide/guide-basics.rst index 73e946881a..ef00ff37ac 100644 --- a/doc/guide/guide-basics.rst +++ b/doc/guide/guide-basics.rst @@ -427,6 +427,7 @@ Of course, like matrices, multiplying two objects of incompatible shape throws a In addition, the logic operators "is equal" `==` and "is not equal" `!=` are also supported. + .. _basics-functions: Functions operating on Qobj class diff --git a/doc/guide/guide-states.rst b/doc/guide/guide-states.rst index 4d4efc3c53..28859a3492 100644 --- a/doc/guide/guide-states.rst +++ b/doc/guide/guide-states.rst @@ -696,6 +696,101 @@ the non-zero component is the zeroth-element of the underlying matrix (remember If one wants to create spin operators for higher spin systems, then the :func:`.jmat` function comes in handy. +.. _quantum_gates: + +Gates +===== + +The pre-defined gates are shown in the table below: + + +.. cssclass:: table-striped + ++------------------------------------------------+-------------------------------------------------------+ +| Gate function | Description | ++================================================+=======================================================+ +| :func:`~qutip.core.gates.rx` | Rotation around x axis | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.ry` | Rotation around y axis | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.rz` | Rotation around z axis | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.sqrtnot` | Square root of not gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.sqrtnot` | Square root of not gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.snot` | Hardmard gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.phasegate` | Phase shift gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.qrot` | A qubit rotation under a Rabi pulse | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.cy_gate` | Controlled y gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.cz_gate` | Controlled z gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.s_gate` | Single-qubit rotation | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.t_gate` | Square root of s gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.cs_gate` | Controlled s gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.ct_gate` | Controlled t gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.cphase` | Controlled phase gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.cnot` | Controlled not gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.csign` | Same as cphase | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.berkeley` | Berkeley gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.swapalpha` | Swapalpha gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.swap` | Swap the states of two qubits | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.iswap` | Swap gate with additional phase for 01 and 10 states | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.sqrtswap` | Square root of the swap gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.sqrtiswap` | Square root of the iswap gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.fredkin` | Fredkin gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.molmer_sorensen` | Molmer Sorensen gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.toffoli` | Toffoli gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.hadamard_transform` | Hadamard gate | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.qubit_clifford_group` | Generates the Clifford group on a single qubit | ++------------------------------------------------+-------------------------------------------------------+ +| :func:`~qutip.core.gates.globalphase` | Global phase gate | ++------------------------------------------------+-------------------------------------------------------+ + +To load this qutip module, first you have to import gates: + +.. code-block:: Python + + from qutip import gates + +For example to use the Hadamard Gate: + +.. testcode:: [basics] + + H = gates.hadamard_transform() + print(H) + +**Output**: + +.. testoutput:: [basics] + :options: +NORMALIZE_WHITESPACE + + Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True + Qobj data = + [[ 0.70710678 0.70710678] + [0.70710678 -0.70710678]] + .. _states-expect: Expectation values @@ -788,6 +883,7 @@ as well as the composite objects discussed in the next section :ref:`tensor`: np.testing.assert_almost_equal(expect(sz2, two_spins), -1) + .. _states-super: Superoperators and Vectorized Operators From 73eaa10ee7bf4485d426d331bca7ba6eff9f561e Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Wed, 12 Jun 2024 17:14:12 +0530 Subject: [PATCH 222/305] add dispatcher for sqrtm --- qutip/core/data/expm.py | 23 ++++++++++++++++++++++- qutip/core/qobj.py | 17 +---------------- qutip/tests/core/data/test_mathematics.py | 11 +++++++++++ 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/qutip/core/data/expm.py b/qutip/core/data/expm.py index 22e69ba5f2..706388ff64 100644 --- a/qutip/core/data/expm.py +++ b/qutip/core/data/expm.py @@ -12,7 +12,7 @@ __all__ = [ 'expm', 'expm_csr', 'expm_csr_dense', 'expm_dense', 'expm_dia', - 'logm', 'logm_dense', + 'logm', 'logm_dense', 'sqrtm', 'sqrtm_dense' ] @@ -129,4 +129,25 @@ def logm_dense(matrix: Dense) -> Dense: (Dense, Dense, logm_dense), ], _defer=True) + +def sqrtm_dense(matrix) -> Dense: + if matrix.shape[0] != matrix.shape[1]: + raise ValueError("can only compute logarithm square matrix") + return Dense(scipy.linalg.sqrtm(matrix.as_ndarray()), copy=False) + + +sqrtm = _Dispatcher( + _inspect.Signature([ + _inspect.Parameter('matrix', _inspect.Parameter.POSITIONAL_ONLY), + ]), + name='sqrtm', + module=__name__, + inputs=('matrix',), + out=True, +) +sqrtm.__doc__ = """Matrix square root `sqrt(A)` for a matrix `A`.""" +sqrtm.add_specialisations([ + (Dense, Dense, sqrtm_dense), +], _defer=True) + del _inspect, _Dispatcher diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 48966c9955..16fe9a251e 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -895,22 +895,7 @@ def sqrtm( """ if self._dims[0] != self._dims[1]: raise TypeError('sqrt only valid on square matrices') - if isinstance(self.data, _data.CSR) and sparse: - evals, evecs = _data.eigs_csr(self.data, - isherm=self._isherm, - tol=tol, maxiter=maxiter) - elif isinstance(self.data, _data.CSR): - evals, evecs = _data.eigs(_data.to(_data.Dense, self.data), - isherm=self._isherm) - else: - evals, evecs = _data.eigs(self.data, isherm=self._isherm) - - dV = _data.diag([np.sqrt(evals, dtype=complex)], 0) - if self.isherm: - spDv = _data.matmul(dV, evecs.conj().transpose()) - else: - spDv = _data.matmul(dV, _data.inv(evecs)) - return Qobj(_data.matmul(evecs, spDv), + return Qobj(_data.sqrtm(self._data), dims=self._dims, copy=False) diff --git a/qutip/tests/core/data/test_mathematics.py b/qutip/tests/core/data/test_mathematics.py index 8d30aa9763..dc53bfbfeb 100644 --- a/qutip/tests/core/data/test_mathematics.py +++ b/qutip/tests/core/data/test_mathematics.py @@ -937,6 +937,17 @@ def op_numpy(self, matrix): ] +class TestSqrtm(UnaryOpMixin): + def op_numpy(self, matrix): + return scipy.linalg.sqrtm(matrix) + + shapes = shapes_square() + bad_shapes = shapes_not_square() + specialisations = [ + pytest.param(data.sqrtm_dense, Dense, Dense), + ] + + class TestTranspose(UnaryOpMixin): def op_numpy(self, matrix): return matrix.T From c9acd9b3acac732f078f192b971c7725a7a21035 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Wed, 12 Jun 2024 17:23:12 +0530 Subject: [PATCH 223/305] add towncrier entry --- doc/changes/2453.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2453.feature diff --git a/doc/changes/2453.feature b/doc/changes/2453.feature new file mode 100644 index 0000000000..ed00922a62 --- /dev/null +++ b/doc/changes/2453.feature @@ -0,0 +1 @@ +Add your info here \ No newline at end of file From 34b60bdba1d9b802362b323a4f1a7e699b3b4f1e Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Wed, 12 Jun 2024 18:18:32 +0530 Subject: [PATCH 224/305] add description in towncrier --- doc/changes/2453.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/2453.feature b/doc/changes/2453.feature index ed00922a62..80636634d2 100644 --- a/doc/changes/2453.feature +++ b/doc/changes/2453.feature @@ -1 +1 @@ -Add your info here \ No newline at end of file +Add dispatcher for sqrtm \ No newline at end of file From 943a996e72b46a721ad6c9f56601d9dacae7086e Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Wed, 12 Jun 2024 10:38:20 -0400 Subject: [PATCH 225/305] add `reduce` --- doc/changes/2454.bugfix | 1 + qutip/solver/integrator/explicit_rk.pyx | 9 +++++++++ qutip/tests/solver/test_integrator.py | 22 ++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 doc/changes/2454.bugfix diff --git a/doc/changes/2454.bugfix b/doc/changes/2454.bugfix new file mode 100644 index 0000000000..be2b44aa32 --- /dev/null +++ b/doc/changes/2454.bugfix @@ -0,0 +1 @@ +Add parallelizing support for `vernN` methods with `mcsolve`. \ No newline at end of file diff --git a/qutip/solver/integrator/explicit_rk.pyx b/qutip/solver/integrator/explicit_rk.pyx index a643de4a44..3417fede39 100644 --- a/qutip/solver/integrator/explicit_rk.pyx +++ b/qutip/solver/integrator/explicit_rk.pyx @@ -199,6 +199,15 @@ cdef class Explicit_RungeKutta: self.b_factor_np = np.empty(self.rk_extra_step, dtype=np.float64) self.b_factor = self.b_factor_np + def __reduce__(self): + """ + Helper for pickle to serialize the object + """ + return (self.__class__, ( + self.qevo, self.rtol, self.atol, self.max_numsteps, self.first_step, + self.min_step, self.max_step, self.interpolate, self.method + )) + cpdef void set_initial_value(self, Data y0, double t) except *: """ Set the initial state and time of the integration. diff --git a/qutip/tests/solver/test_integrator.py b/qutip/tests/solver/test_integrator.py index e87f334ba6..93491ff43e 100644 --- a/qutip/tests/solver/test_integrator.py +++ b/qutip/tests/solver/test_integrator.py @@ -166,3 +166,25 @@ def test_concurent_usage(integrator): assert inter1.integrate(t)[1].to_array()[0, 0] == expected1 expected2 = pytest.approx(np.exp(-t/2), abs=1e-5) assert inter2.integrate(t)[1].to_array()[0, 0] == expected2 + +@pytest.mark.parametrize('integrator', + [IntegratorVern7, IntegratorVern9], + ids=["vern7", 'vern9'] +) +def test_pickling_vern_methods(integrator): + """Test whether VernN methods can be pickled and hence used in multiprocessing""" + opt = {'atol':1e-10, 'rtol':1e-7} + + sys = qutip.QobjEvo(0.5*qutip.qeye(1)) + inter = integrator(sys, opt) + inter.set_state(0, qutip.basis(1,0).data) + + import pickle + pickled = pickle.dumps(inter, -1) + recreated = pickle.loads(pickled) + + for t in np.linspace(0,1,6): + expected = pytest.approx(np.exp(t/2), abs=1e-5) + result1 = inter.integrate(t)[1].to_array()[0, 0] + result2 = recreated.integrate(t)[1].to_array()[0, 0] + assert result1 == result2 == expected From fa6ef326849df8c5b3caa94986c79e708fc927a6 Mon Sep 17 00:00:00 2001 From: obliviateandsurrender Date: Wed, 12 Jun 2024 12:34:09 -0400 Subject: [PATCH 226/305] fix test --- qutip/tests/solver/test_integrator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutip/tests/solver/test_integrator.py b/qutip/tests/solver/test_integrator.py index 93491ff43e..f7a05ff551 100644 --- a/qutip/tests/solver/test_integrator.py +++ b/qutip/tests/solver/test_integrator.py @@ -182,6 +182,7 @@ def test_pickling_vern_methods(integrator): import pickle pickled = pickle.dumps(inter, -1) recreated = pickle.loads(pickled) + recreated.set_state(0, qutip.basis(1,0).data) for t in np.linspace(0,1,6): expected = pytest.approx(np.exp(t/2), abs=1e-5) From 91491525066472be799e9a6c4b57200b4a41958e Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 12 Jun 2024 17:01:51 -0400 Subject: [PATCH 227/305] Fix bugs --- qutip/solver/mcsolve.py | 3 ++- qutip/solver/multitrajresult.py | 11 +++++------ qutip/tests/solver/test_mcsolve.py | 3 +++ 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 3487558477..68f9b8caf8 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -532,7 +532,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): Run one trajectory and return the result. """ jump_prob_floor = integrator_kwargs.get('jump_prob_floor', 0) - if jump_prob_floor == 1: + if jump_prob_floor >= 1 - self.options["norm_tol"]: # The no-jump probability is one, but we are asked to generate # a trajectory with at least one jump. # This can happen when a user uses "improved sampling" with a dark @@ -543,6 +543,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): zero = qzero_like(self._restore_state(state, copy=False)) result = self._trajectory_resultclass(e_ops, self.options) result.collapse = [] + result.add_relative_weight(0) for t in tlist: result.add(t, zero) return seed, result diff --git a/qutip/solver/multitrajresult.py b/qutip/solver/multitrajresult.py index 77de65b3fe..68d4b0e101 100644 --- a/qutip/solver/multitrajresult.py +++ b/qutip/solver/multitrajresult.py @@ -341,8 +341,8 @@ def _target_tolerance_end(self): # and "<1>" is one minus the sum of all absolute weights one = one - self._total_abs_weight - target_ntraj = np.max((avg2 / one - (abs(avg) ** 2) / (one ** 2)) / - target**2 + 1) + std = avg2 - abs(avg)**2 + target_ntraj = np.max(std / target**2) * one**2 + 1 self._estimated_ntraj = min(target_ntraj - self._num_rel_trajectories, self._target_ntraj - self.num_trajectories) @@ -635,8 +635,7 @@ def merge(self, other, p=None): where p is a parameter between 0 and 1. Its default value is :math:`p_{\textrm{def}} = N / (N + N')`, N and N' being the number of - trajectories in the two result objects. (In the case of weighted - trajectories, only trajectories without absolute weights are counted.) + trajectories in the two result objects. Parameters ---------- @@ -665,7 +664,7 @@ def merge(self, other, p=None): other._num_rel_trajectories) new.seeds = self.seeds + other.seeds - p_equal = self._num_rel_trajectories / new._num_rel_trajectories + p_equal = self.num_trajectories / new.num_trajectories if p is None: p = p_equal @@ -1125,7 +1124,7 @@ def trace(self): def merge(self, other, p=None): new = super().merge(other, p) - p_eq = self._num_rel_trajectories / new._num_rel_trajectories + p_eq = self.num_trajectories / new.num_trajectories if p is None: p = p_eq diff --git a/qutip/tests/solver/test_mcsolve.py b/qutip/tests/solver/test_mcsolve.py index e106798ae3..3566e69b2a 100644 --- a/qutip/tests/solver/test_mcsolve.py +++ b/qutip/tests/solver/test_mcsolve.py @@ -604,6 +604,7 @@ def test_mixed_averaging(improved_sampling, initial_state, ntraj): assert result.ntraj_per_initial_state == ntraj else: assert sum(result.ntraj_per_initial_state) == ntraj + assert sum(result.runs_weights) == pytest.approx(1.) @pytest.mark.parametrize("improved_sampling", [True, False]) @@ -648,3 +649,5 @@ def test_mixed_equals_merged(improved_sampling, p): assert hasattr(mixed_result, 'ntraj_per_initial_state') assert isinstance(mixed_result.ntraj_per_initial_state, list) assert mixed_result.ntraj_per_initial_state == ntraj + assert sum(mixed_result.runs_weights) == pytest.approx(1.) + assert sum(merged_result.runs_weights) == pytest.approx(1.) From a206fbf996979a0aba4db6bdedebd73337db67be Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 13 Jun 2024 09:27:19 -0400 Subject: [PATCH 228/305] Numpy 2 support - Update dependency - Add more test using numpy 2 - Fix sqrtm needing copy --- .github/workflows/tests.yml | 102 ++++++++++++++++++------------------ pyproject.toml | 4 +- qutip/core/data/expm.py | 2 +- setup.cfg | 12 ++--- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f5298049dd..c76c2446ef 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,31 +29,35 @@ jobs: # matrix size; make sure to test all supported versions in some form. python-version: ["3.11"] case-name: [defaults] - numpy-requirement: [">=1.22"] - scipy-requirement: [">=1.8"] + numpy-build: [""] # On conda, the prerelease is tagged as 2.0 + numpy-requirement: [""] + scipy-requirement: [">=1.9"] coverage-requirement: ["==6.5"] + # pypi: 1 # numpy 2 not yet available on conda # Extra special cases. In these, the new variable defined should always # be a truth-y value (hence 'nomkl: 1' rather than 'mkl: 0'), because # the lack of a variable is _always_ false-y, and the defaults lack all # the special cases. include: # Test with the version 2 of numpy comming soon. - - case-name: numpy2 + - case-name: python 3.12 os: ubuntu-latest python-version: "3.12" - scipy-requirement: "" - numpy-requirement: "==2.0.0rc2" + numpy-build: ">=2.0.0rc2" pypi: 1 # Binaries compiled with numpy 2 should be compatible when using # numpy 1.X at runtime. - - case-name: numpy2_to_1 + - case-name: build numpy 1.22 os: ubuntu-latest python-version: "3.10" scipy-requirement: "" - numpy-requirement: "==2.0.0rc2" - roll_back_numpy: 1 - pypi: 1 # numpy 2 not yet available on conda + numpy-requirement: ">=1.22.0,<1.23.0" + numpy-build: ">=1.22.0,<1.23.0" + semidefinite: 1 + oldcython: 1 + pypi: 1 + pytest-extra-options: "-W ignore:dep_util:DeprecationWarning -W \"ignore:The 'renderer' parameter of do_3d_projection\"" # Python 3.10, no mkl, scipy 1.9, numpy 1.23 # Scipy 1.9 did not support cython 3.0 yet. @@ -63,33 +67,11 @@ jobs: python-version: "3.10" scipy-requirement: ">=1.9,<1.10" numpy-requirement: ">=1.23,<1.24" + semidefinite: 1 condaforge: 1 oldcython: 1 nomkl: 1 - pytest-extra-options: "-W ignore:dep_util:DeprecationWarning" - - # Python 3.10, no cython, scipy 1.10, numpy 1.24 - - case-name: no cython - os: ubuntu-latest - python-version: "3.10" - scipy-requirement: ">=1.10,<1.11" - numpy-requirement: ">=1.24,<1.25" - oldcython: 1 - nocython: 1 - - # Python 3.11 and recent numpy - # Use conda-forge to provide Python 3.11 and latest numpy - # Ignore deprecation of the cgi module in Python 3.11 that is - # still imported by Cython.Tempita. This was addressed in - # https://github.com/cython/cython/pull/5128 but not backported - # to any currently released version. - - case-name: Python 3.11 - os: ubuntu-latest - python-version: "3.11" - condaforge: 1 - scipy-requirement: ">=1.11,<1.12" - numpy-requirement: ">=1.25,<1.26" - conda-extra-pkgs: "suitesparse" # for compiling cvxopt + pytest-extra-options: "-W ignore:dep_util:DeprecationWarning -W \"ignore:The 'renderer' parameter of do_3d_projection\"" # Python 3.12 and latest numpy # Use conda-forge to provide Python 3.11 and latest numpy @@ -99,7 +81,6 @@ jobs: scipy-requirement: ">=1.12,<1.13" numpy-requirement: ">=1.26,<1.27" condaforge: 1 - pytest-extra-options: "-W ignore:datetime:DeprecationWarning" # Install mpi4py to test mpi_pmap # Should be enough to include this in one of the runs includempi: 1 @@ -109,21 +90,34 @@ jobs: - case-name: macos # setup-miniconda not compatible with macos-latest presently. # https://github.com/conda-incubator/setup-miniconda/issues/344 + os: macos-12 + python-version: "3.12" + condaforge: 1 + nomkl: 1 + + - case-name: macos - numpy 1.25 os: macos-12 python-version: "3.11" + scipy-requirement: ">=1.11,<1.12" + numpy-requirement: ">=1.25,<1.26" + # conda-extra-pkgs: "suitesparse" # for compiling cvxopt condaforge: 1 nomkl: 1 - # Windows. Once all tests pass without special options needed, this - # can be moved to the main os list in the test matrix. All the tests - # that fail currently seem to do so because mcsolve uses - # multiprocessing under the hood. Windows does not support fork() - # well, which makes transfering objects to the child processes - # error prone. See, e.g., https://github.com/qutip/qutip/issues/1202 - case-name: Windows os: windows-latest python-version: "3.11" + - case-name: Windows - No cython + os: windows-latest + python-version: "3.10" + scipy-requirement: ">=1.10,<1.11" + numpy-requirement: ">=1.24,<1.25" + semidefinite: 1 + oldcython: 1 + nocython: 1 + pytest-extra-options: "-W ignore:dep_util:DeprecationWarning -W \"ignore:The 'renderer' parameter of do_3d_projection\"" + steps: - uses: actions/checkout@v4 - uses: conda-incubator/setup-miniconda@v3 @@ -140,25 +134,29 @@ jobs: # version of cython, scipy, numpy in the test matrix, not a temporary # version use in the installation virtual environment. run: | + conda config --add channels conda-forge/label/numpy_rc # Install the extra requirement python -m pip install pytest>=5.2 pytest-rerunfailures # tests python -m pip install matplotlib>=1.2.1 # graphics - python -m pip install cvxpy>=1.0 cvxopt # semidefinite python -m pip install ipython # ipython python -m pip install loky tqdm # extras python -m pip install "coverage${{ matrix.coverage-requirement }}" chardet python -m pip install pytest-cov coveralls pytest-fail-slow if [[ "${{ matrix.pypi }}" ]]; then - pip install "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" + pip install "numpy${{ matrix.numpy-build }}" + pip install "scipy${{ matrix.scipy-requirement }}" elif [[ -z "${{ matrix.nomkl }}" ]]; then - conda install blas=*=mkl "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" + conda install blas=*=mkl "numpy${{ matrix.numpy-build }}" "scipy${{ matrix.scipy-requirement }}" elif [[ "${{ matrix.os }}" =~ ^windows.*$ ]]; then # Conda doesn't supply forced nomkl builds on Windows, so we rely on # pip not automatically linking to MKL. - pip install "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" + pip install "numpy${{ matrix.numpy-build }}" "scipy${{ matrix.scipy-requirement }}" else - conda install nomkl "numpy${{ matrix.numpy-requirement }}" "scipy${{ matrix.scipy-requirement }}" + conda install nomkl "numpy${{ matrix.numpy-build }}" "scipy${{ matrix.scipy-requirement }}" + fi + if [[ -n "${{ matrix.semidefinite }}" ]]; then + python -m pip install cvxpy>=1.0 cvxopt fi if [[ -n "${{ matrix.conda-extra-pkgs }}" ]]; then conda install "${{ matrix.conda-extra-pkgs }}" @@ -168,7 +166,7 @@ jobs: conda install "openmpi<5" mpi4py fi if [[ "${{ matrix.oldcython }}" ]]; then - python -m pip install cython==0.29.36 filelock + python -m pip install cython==0.29.36 filelock matplotlib==3.5 else python -m pip install cython filelock fi @@ -179,13 +177,17 @@ jobs: python -m pip uninstall cython -y fi - if [[ "${{ matrix.roll_back_numpy }}" ]]; then - # Binary compiled with numpy 2.X should be compatible with numpy 1.X - python -m pip install "numpy<1.24" + if [[ "${{ matrix.pypi }}" ]]; then + python -m pip install "numpy${{ matrix.numpy-requirement }}" --pre + elif [[ -z "${{ matrix.nomkl }}" ]]; then + conda install "numpy${{ matrix.numpy-requirement }}" + elif [[ "${{ matrix.os }}" =~ ^windows.*$ ]]; then + python -m pip install "numpy${{ matrix.numpy-requirement }}" --pre + else + conda install nomkl "numpy${{ matrix.numpy-requirement }}" fi - - name: Package information run: | conda list diff --git a/pyproject.toml b/pyproject.toml index d9f4b90716..6932f34ba8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,8 @@ requires = [ "cython>=0.29.20,<3.0.0; python_version<='3.9'", # See https://numpy.org/doc/stable/user/depending_on_numpy.html for # the recommended way to build against numpy's C API: - "oldest-supported-numpy", - "scipy>=1.8", + "numpy>=2.0.0rc2", + "scipy>=1.9", ] build-backend = "setuptools.build_meta" diff --git a/qutip/core/data/expm.py b/qutip/core/data/expm.py index 706388ff64..fb0422326e 100644 --- a/qutip/core/data/expm.py +++ b/qutip/core/data/expm.py @@ -133,7 +133,7 @@ def logm_dense(matrix: Dense) -> Dense: def sqrtm_dense(matrix) -> Dense: if matrix.shape[0] != matrix.shape[1]: raise ValueError("can only compute logarithm square matrix") - return Dense(scipy.linalg.sqrtm(matrix.as_ndarray()), copy=False) + return Dense(scipy.linalg.sqrtm(matrix.as_ndarray())) sqrtm = _Dispatcher( diff --git a/setup.cfg b/setup.cfg index c54f59c7b9..ceeb15b193 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,24 +32,22 @@ include_package_data = True zip_safe = False python_requires = >=3.10 install_requires = - numpy>=1.22,<2.0.0 - scipy>=1.8 + numpy>=1.22 + scipy>=1.9 packaging setup_requires = - numpy>=1.19,<2.0.0 - scipy>=1.8 + numpy>=2.0.0rc2 + scipy>=1.9 cython>=0.29.20; python_version>='3.10' - cython>=0.29.20,<3.0.0; python_version<='3.9' packaging [options.packages.find] include = qutip* [options.extras_require] -graphics = matplotlib>=1.2.1 +graphics = matplotlib>=3.5 runtime_compilation = cython>=0.29.20; python_version>='3.10' - cython>=0.29.20,<3.0.0; python_version<='3.9' filelock setuptools semidefinite = From e143803e45153ddb083846e6562ac62e91902606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Fri, 14 Jun 2024 11:27:27 -0400 Subject: [PATCH 229/305] Apply suggestions from code review Co-authored-by: Simon Cross --- doc/guide/guide-settings.rst | 20 ++++++++++---------- qutip/core/coefficient.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/doc/guide/guide-settings.rst b/doc/guide/guide-settings.rst index 5f7b176b01..f6424a7daf 100644 --- a/doc/guide/guide-settings.rst +++ b/doc/guide/guide-settings.rst @@ -6,7 +6,7 @@ QuTiP settings QuTiP has multiple settings that control it's behaviour: -* ``qutip.settings`` contain installation and runtime information. +* ``qutip.settings`` contains installation and runtime information. Most of these parameters are readonly. But systems paths used by QuTiP are also included here and could need updating in none standard environment. * ``qutip.settings.core`` contains options for operations with ``Qobj`` and @@ -34,18 +34,18 @@ Environment settings +-------------------+-----------+----------------------------------------------------------+ | `mkl_lib` | True | Path of the mkl libraries found. | +-------------------+-----------+----------------------------------------------------------+ -| `ipython` | True | Whether running in ipython. | +| `ipython` | True | Whether running in IPython. | +-------------------+-----------+----------------------------------------------------------+ -| `eigh_unsafe` | True | Whether to use eig for hermitian matrix since it can | -| | | segfault in some conditions. | +| `eigh_unsafe` | True | When true, SciPy's `eigh` and `eigvalsh` are replaced with custom implementations that call `eig` and `eigvals` instead. | +| | This setting exists because in some environments SciPy's `eigh` segfaults or gives invalid results. | | +-------------------+-----------+----------------------------------------------------------+ -| `coeffroot` | False | Directory in which QuTiP creates cython module for | +| `coeffroot` | False | Directory in which QuTiP creates cython modules for | | | | string coefficient. | +-------------------+-----------+----------------------------------------------------------+ | `coeff_write_ok` | True | Whether QuTiP has write permission for `coeffroot`. | +-------------------+-----------+----------------------------------------------------------+ | `idxint_size` | True | Whether QuTiP's sparse matrix indices use 32 or 64 bits. | -| | | Sparse matrices' size are limited to 2**(idxint_size-1). | +| | | Sparse matrices' size are limited to 2**(idxint_size-1) rows and columns. | +-------------------+-----------+----------------------------------------------------------+ | `num_cpus` | True | Detected number of cpus. | +-------------------+-----------+----------------------------------------------------------+ @@ -57,7 +57,7 @@ It may be needed to update ``coeffroot`` if the default HOME is not writable. It >>> qutip.settings.coeffroot = "path/to/string/coeff/directory" -New to version 5, string compiled in a session are kept for future sessions. +In QuTiP version 5 and later, strings compiled in a session are kept for future sessions. As long as the same ``coeffroot`` is used, each string will only be compiled once. @@ -172,7 +172,7 @@ Lastly some options control how qutip tries to detect C types (for advanced user +--------------------------+-----------------------------------------------------------------------------------------+ | Options | Description | +==========================+=========================================================================================+ -| `try_parse` | Whether qutip parse the string to detect common patterns. | +| `try_parse` | Whether QuTiP parses the string to detect common patterns. | | | | | | When True, "cos(w * t)" and "cos(a * t)" will use the same compiled coefficient. | +--------------------------+-----------------------------------------------------------------------------------------+ @@ -180,11 +180,11 @@ Lastly some options control how qutip tries to detect C types (for advanced user | | | | | If True, scalar (int, float, complex), string and Data types are detected. | +--------------------------+-----------------------------------------------------------------------------------------+ -| `accept_int` | Whether to type ``args`` values which are python int as int or float/complex. | +| `accept_int` | Whether to type ``args`` values which are Python ints as int or float/complex. | | | | | | Per default it is True when subscription (``a[i]``) is used. | +--------------------------+-----------------------------------------------------------------------------------------+ -| `accept_float` | Whether to type ``args`` values which are python float as int or float/complex. | +| `accept_float` | Whether to type ``args`` values which are Python floats as int or float/complex. | | | | | | Per default it is True when comparison (``a > b``) is used. | +--------------------------+-----------------------------------------------------------------------------------------+ diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index b438442757..c2af1e71a8 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -403,7 +403,7 @@ def coeff_from_str(base, args, args_ctypes, compile_opt=None, **_): if not compile_opt['use_cython']: if WARN_MISSING_MODULE[0]: warnings.warn( - "`cython` `setuptools` and `filelock` are required for " + "`cython`, `setuptools` and `filelock` are required for " "compilation of string coefficents. Falling back on `eval`.") # Only warns once. WARN_MISSING_MODULE[0] = 0 From 490f12f8fe6736c69ad875aadb9b76de1bba92ad Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 14 Jun 2024 11:31:44 -0400 Subject: [PATCH 230/305] Fix table line width --- doc/guide/guide-settings.rst | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/doc/guide/guide-settings.rst b/doc/guide/guide-settings.rst index f6424a7daf..8411a718a8 100644 --- a/doc/guide/guide-settings.rst +++ b/doc/guide/guide-settings.rst @@ -36,16 +36,20 @@ Environment settings +-------------------+-----------+----------------------------------------------------------+ | `ipython` | True | Whether running in IPython. | +-------------------+-----------+----------------------------------------------------------+ -| `eigh_unsafe` | True | When true, SciPy's `eigh` and `eigvalsh` are replaced with custom implementations that call `eig` and `eigvals` instead. | -| | This setting exists because in some environments SciPy's `eigh` segfaults or gives invalid results. | | +| `eigh_unsafe` | True | When true, SciPy's `eigh` and `eigvalsh` are replaced | +| | | with custom implementations that call `eig` and | +| | | `eigvals` instead. This setting exists because in some | +| | | environments SciPy's `eigh` segfaults or gives invalid | +| | | results. | +-------------------+-----------+----------------------------------------------------------+ -| `coeffroot` | False | Directory in which QuTiP creates cython modules for | +| `coeffroot` | False | Directory in which QuTiP creates cython modules for | | | | string coefficient. | +-------------------+-----------+----------------------------------------------------------+ | `coeff_write_ok` | True | Whether QuTiP has write permission for `coeffroot`. | +-------------------+-----------+----------------------------------------------------------+ | `idxint_size` | True | Whether QuTiP's sparse matrix indices use 32 or 64 bits. | -| | | Sparse matrices' size are limited to 2**(idxint_size-1) rows and columns. | +| | | Sparse matrices' size are limited to 2**(idxint_size-1) | +| | | rows and columns. | +-------------------+-----------+----------------------------------------------------------+ | `num_cpus` | True | Detected number of cpus. | +-------------------+-----------+----------------------------------------------------------+ @@ -172,7 +176,7 @@ Lastly some options control how qutip tries to detect C types (for advanced user +--------------------------+-----------------------------------------------------------------------------------------+ | Options | Description | +==========================+=========================================================================================+ -| `try_parse` | Whether QuTiP parses the string to detect common patterns. | +| `try_parse` | Whether QuTiP parses the string to detect common patterns. | | | | | | When True, "cos(w * t)" and "cos(a * t)" will use the same compiled coefficient. | +--------------------------+-----------------------------------------------------------------------------------------+ @@ -180,11 +184,11 @@ Lastly some options control how qutip tries to detect C types (for advanced user | | | | | If True, scalar (int, float, complex), string and Data types are detected. | +--------------------------+-----------------------------------------------------------------------------------------+ -| `accept_int` | Whether to type ``args`` values which are Python ints as int or float/complex. | +| `accept_int` | Whether to type ``args`` values which are Python ints as int or float/complex. | | | | | | Per default it is True when subscription (``a[i]``) is used. | +--------------------------+-----------------------------------------------------------------------------------------+ -| `accept_float` | Whether to type ``args`` values which are Python floats as int or float/complex. | +| `accept_float` | Whether to type ``args`` values which are Python floats as int or float/complex. | | | | | | Per default it is True when comparison (``a > b``) is used. | +--------------------------+-----------------------------------------------------------------------------------------+ From e61b4888a39a8cf938bb7452a295635aa983fd29 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 17 Jun 2024 08:56:42 -0400 Subject: [PATCH 231/305] Use official release --- .github/workflows/tests.yml | 6 ++---- pyproject.toml | 2 +- setup.cfg | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c76c2446ef..60a1b079cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,11 +29,10 @@ jobs: # matrix size; make sure to test all supported versions in some form. python-version: ["3.11"] case-name: [defaults] - numpy-build: [""] # On conda, the prerelease is tagged as 2.0 + numpy-build: [""] numpy-requirement: [""] scipy-requirement: [">=1.9"] coverage-requirement: ["==6.5"] - # pypi: 1 # numpy 2 not yet available on conda # Extra special cases. In these, the new variable defined should always # be a truth-y value (hence 'nomkl: 1' rather than 'mkl: 0'), because # the lack of a variable is _always_ false-y, and the defaults lack all @@ -43,7 +42,7 @@ jobs: - case-name: python 3.12 os: ubuntu-latest python-version: "3.12" - numpy-build: ">=2.0.0rc2" + numpy-build: ">=2.0.0" pypi: 1 # Binaries compiled with numpy 2 should be compatible when using @@ -134,7 +133,6 @@ jobs: # version of cython, scipy, numpy in the test matrix, not a temporary # version use in the installation virtual environment. run: | - conda config --add channels conda-forge/label/numpy_rc # Install the extra requirement python -m pip install pytest>=5.2 pytest-rerunfailures # tests python -m pip install matplotlib>=1.2.1 # graphics diff --git a/pyproject.toml b/pyproject.toml index 6932f34ba8..888bbb161e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires = [ "cython>=0.29.20,<3.0.0; python_version<='3.9'", # See https://numpy.org/doc/stable/user/depending_on_numpy.html for # the recommended way to build against numpy's C API: - "numpy>=2.0.0rc2", + "numpy>=2.0.0", "scipy>=1.9", ] build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index ceeb15b193..dfd09c1964 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = scipy>=1.9 packaging setup_requires = - numpy>=2.0.0rc2 + numpy>=2.0.0 scipy>=1.9 cython>=0.29.20; python_version>='3.10' packaging From 076cc34c271a3647195bb71463d4fc143838a0d0 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 17 Jun 2024 08:58:29 -0400 Subject: [PATCH 232/305] Add towncrier --- doc/changes/2457.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2457.misc diff --git a/doc/changes/2457.misc b/doc/changes/2457.misc new file mode 100644 index 0000000000..8ab04dd93a --- /dev/null +++ b/doc/changes/2457.misc @@ -0,0 +1 @@ +Add numpy 2 support From b6fbda94a8a06a28ac4a6d445c5ef2d300203046 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 17 Jun 2024 10:24:25 -0400 Subject: [PATCH 233/305] Default don't use numpy 2 --- .github/workflows/tests.yml | 61 ++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 60a1b079cc..b80c6a19e4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,6 +29,8 @@ jobs: # matrix size; make sure to test all supported versions in some form. python-version: ["3.11"] case-name: [defaults] + # Version 2 not yet available on conda's default channel + condaforge: [1] numpy-build: [""] numpy-requirement: [""] scipy-requirement: [">=1.9"] @@ -38,52 +40,50 @@ jobs: # the lack of a variable is _always_ false-y, and the defaults lack all # the special cases. include: - # Test with the version 2 of numpy comming soon. - - case-name: python 3.12 + - case-name: p312 numpy 2 os: ubuntu-latest python-version: "3.12" numpy-build: ">=2.0.0" + numpy-requirement: ">=2.0.0" pypi: 1 - # Binaries compiled with numpy 2 should be compatible when using - # numpy 1.X at runtime. - - case-name: build numpy 1.22 + - case-name: p310 numpy 1.22 os: ubuntu-latest python-version: "3.10" - scipy-requirement: "" - numpy-requirement: ">=1.22.0,<1.23.0" numpy-build: ">=1.22.0,<1.23.0" + numpy-requirement: ">=1.22.0,<1.23.0" + scipy-requirement: ">=1.10,<1.11" semidefinite: 1 oldcython: 1 pypi: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning -W \"ignore:The 'renderer' parameter of do_3d_projection\"" + # Python 3.12 and latest numpy + # Use conda-forge to provide Python 3.11 and latest numpy + - case-name: p312, numpy fallback + os: ubuntu-latest + python-version: "3.12" + numpy-requirement: ">=1.26,<1.27" + scipy-requirement: ">=1.11,<1.12" + condaforge: 1 + # Install mpi4py to test mpi_pmap + # Should be enough to include this in one of the runs + includempi: 1 + # Python 3.10, no mkl, scipy 1.9, numpy 1.23 # Scipy 1.9 did not support cython 3.0 yet. # cython#17234 - - case-name: no mkl + - case-name: p310 no mkl os: ubuntu-latest python-version: "3.10" - scipy-requirement: ">=1.9,<1.10" numpy-requirement: ">=1.23,<1.24" + scipy-requirement: ">=1.9,<1.10" semidefinite: 1 condaforge: 1 oldcython: 1 nomkl: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning -W \"ignore:The 'renderer' parameter of do_3d_projection\"" - # Python 3.12 and latest numpy - # Use conda-forge to provide Python 3.11 and latest numpy - - case-name: Python 3.12 - os: ubuntu-latest - python-version: "3.12" - scipy-requirement: ">=1.12,<1.13" - numpy-requirement: ">=1.26,<1.27" - condaforge: 1 - # Install mpi4py to test mpi_pmap - # Should be enough to include this in one of the runs - includempi: 1 - # Mac # Mac has issues with MKL since september 2022. - case-name: macos @@ -91,30 +91,35 @@ jobs: # https://github.com/conda-incubator/setup-miniconda/issues/344 os: macos-12 python-version: "3.12" + numpy-build: ">=2.0.0" + numpy-requirement: ">=2.0.0" condaforge: 1 nomkl: 1 - - case-name: macos - numpy 1.25 + - case-name: macos - numpy fallback os: macos-12 python-version: "3.11" - scipy-requirement: ">=1.11,<1.12" + numpy-build: ">=2.0.0" numpy-requirement: ">=1.25,<1.26" - # conda-extra-pkgs: "suitesparse" # for compiling cvxopt condaforge: 1 nomkl: 1 - case-name: Windows os: windows-latest python-version: "3.11" + numpy-build: ">=2.0.0" + numpy-requirement: ">=2.0.0" + pypi: 1 - - case-name: Windows - No cython + - case-name: Windows - numpy fallback os: windows-latest python-version: "3.10" - scipy-requirement: ">=1.10,<1.11" + numpy-build: ">=2.0.0" numpy-requirement: ">=1.24,<1.25" semidefinite: 1 oldcython: 1 nocython: 1 + condaforge: 1 pytest-extra-options: "-W ignore:dep_util:DeprecationWarning -W \"ignore:The 'renderer' parameter of do_3d_projection\"" steps: @@ -176,11 +181,11 @@ jobs: fi if [[ "${{ matrix.pypi }}" ]]; then - python -m pip install "numpy${{ matrix.numpy-requirement }}" --pre + python -m pip install "numpy${{ matrix.numpy-requirement }}" elif [[ -z "${{ matrix.nomkl }}" ]]; then conda install "numpy${{ matrix.numpy-requirement }}" elif [[ "${{ matrix.os }}" =~ ^windows.*$ ]]; then - python -m pip install "numpy${{ matrix.numpy-requirement }}" --pre + python -m pip install "numpy${{ matrix.numpy-requirement }}" else conda install nomkl "numpy${{ matrix.numpy-requirement }}" fi From 81484756d93f3c14561377449efb0b4f1cd2395a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:24:52 +0000 Subject: [PATCH 234/305] Bump urllib3 from 1.26.18 to 1.26.19 in /doc Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.18 to 1.26.19. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/1.26.19/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.26.18...1.26.19) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index a2b96718b0..c0b500a983 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -43,6 +43,6 @@ sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 traitlets==5.9.0 -urllib3==1.26.18 +urllib3==1.26.19 wcwidth==0.2.6 wheel==0.38.4 From 21f52dee13481f033184833da8657995ab115c8b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 18 Jun 2024 11:00:07 -0400 Subject: [PATCH 235/305] Fix traj bugs --- qutip/solver/multitrajresult.py | 76 ++++++++++++++++++++++++------ qutip/tests/solver/test_results.py | 37 +++++++-------- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/qutip/solver/multitrajresult.py b/qutip/solver/multitrajresult.py index 68d4b0e101..52a12b0e8a 100644 --- a/qutip/solver/multitrajresult.py +++ b/qutip/solver/multitrajresult.py @@ -337,13 +337,16 @@ def _target_tolerance_end(self): # We only include traj. without abs. weights in this calculation. # Since there are traj. with abs. weights., the weights don't add # up to one. We have to consider that as follows: - # <(x - )^2> / <1> = / <1> - ^2 / <1>^2 + # err = (std * **2 / (N-1)) ** 0.5 + # avg = + # avg2 = + # std * **2 = ( - **2) * **2 + # = avg2 * - avg**2 # and "<1>" is one minus the sum of all absolute weights one = one - self._total_abs_weight - std = avg2 - abs(avg)**2 - target_ntraj = np.max(std / target**2) * one**2 + 1 - + std = avg2 * one - abs(avg)**2 + target_ntraj = np.max(std / target**2) + 1 self._estimated_ntraj = min(target_ntraj - self._num_rel_trajectories, self._target_ntraj - self.num_trajectories) if self._estimated_ntraj <= 0: @@ -522,11 +525,29 @@ def average_final_state(self): """ Last states of each trajectories averaged into a density matrix. """ - if ((self._sum_abs and not self._sum_abs.sum_final_state) or - (self._sum_rel and not self._sum_rel.sum_final_state)): - if (average_states := self.average_states) is not None: - return average_states[-1] - return None + trajectory_states_available = (self.trajectories and + self.trajectories[0].final_state) + states = self.average_states + need_to_reduce_states = False + if self._sum_abs and not self._sum_abs.sum_final_state: + if not (trajectory_states_available or states): + return None + need_to_reduce_states = True + + if self._sum_rel and not self._sum_rel.sum_final_state: + if not (trajectory_states_available or states): + return None + need_to_reduce_states = True + + if need_to_reduce_states and states: + return states[-1] + elif need_to_reduce_states: + if self._sum_abs: + self._sum_abs._initialize_sum_finalstate(self.trajectories[0]) + if self._sum_rel: + self._sum_rel._initialize_sum_finalstate(self.trajectories[0]) + for trajectory in self.trajectories: + self._reduce_final_state(trajectory) if self._sum_abs and self._sum_rel: return (self._sum_abs.sum_final_state + @@ -659,19 +680,42 @@ def merge(self, other, p=None): new.times = self.times new.e_ops = self.e_ops + if bool(self.trajectories) != bool(other.trajectories): + # ensure the states are reduced. + if self.trajectories: + self.average_states + self.average_final_state + else: + other.average_states + other.average_final_state + new.num_trajectories = self.num_trajectories + other.num_trajectories new._num_rel_trajectories = (self._num_rel_trajectories + other._num_rel_trajectories) new.seeds = self.seeds + other.seeds - p_equal = self.num_trajectories / new.num_trajectories + p_equal = self._num_rel_trajectories / new._num_rel_trajectories if p is None: - p = p_equal + p = self.num_trajectories / new.num_trajectories if self.trajectories and other.trajectories: new.trajectories = self._merge_trajectories(other, p, p_equal) else: new._weight_info = self._merge_weight_info(other, p, p_equal) + new.trajectories = [] + new.options["keep_runs_results"] = False + new.runs_e_data = {} + + self_states = self.options["store_states"] + self_fstate = self.options["store_final_state"] + other_states = other.options["store_states"] + other_fstate = other.options["store_final_state"] + + new.options["store_states"] = self_states and other_states + + new.options["store_final_state"] = ( + (self_fstate or self_states) and (other_fstate or other_states) + ) new._sum_abs = _TrajectorySum.merge( self._sum_abs, p, other._sum_abs, 1 - p) @@ -682,7 +726,6 @@ def merge(self, other, p=None): new._create_e_data() if self.runs_e_data and other.runs_e_data: - new.runs_e_data = {} for k in self._raw_ops: new.runs_e_data[k] = self.runs_e_data[k] + other.runs_e_data[k] @@ -803,6 +846,11 @@ def _initialize_sum_states(self, example_trajectory): self.sum_states = [ qzero_like(_to_dm(state)) for state in example_trajectory.states] + def _initialize_sum_finalstate(self, example_trajectory): + self.sum_final_state = qzero_like( + _to_dm(example_trajectory.final_state) + ) + def reduce_states(self, trajectory): """ Adds the states stored in the given trajectory to the running sum @@ -1124,9 +1172,9 @@ def trace(self): def merge(self, other, p=None): new = super().merge(other, p) - p_eq = self.num_trajectories / new.num_trajectories + p_eq = self._num_rel_trajectories / new._num_rel_trajectories if p is None: - p = p_eq + p = self.num_trajectories / new.num_trajectories new._sum_trace_abs = ( self._merge_weight(p, p_eq, True) * self._sum_trace_abs + diff --git a/qutip/tests/solver/test_results.py b/qutip/tests/solver/test_results.py index b638bc2342..540f45fe06 100644 --- a/qutip/tests/solver/test_results.py +++ b/qutip/tests/solver/test_results.py @@ -338,7 +338,9 @@ def test_multitraj_expect(self, keep_runs_results, include_no_jump, e_ops, results): N = 5 ntraj = 25 - opt = fill_options(keep_runs_results=keep_runs_results) + opt = fill_options( + keep_runs_results=keep_runs_results, store_final_state=True + ) m_res = MultiTrajResult(e_ops, opt, stats={}) self._fill_trajectories(m_res, N, ntraj, noise=0.01, include_no_jump=include_no_jump) @@ -358,7 +360,7 @@ def test_multitraj_expect(self, keep_runs_results, include_no_jump, atol=1e-14, rtol=0.1) self._check_types(m_res) - + assert m_res.average_final_state is not None assert m_res.stats['end_condition'] == "unknown" @pytest.mark.parametrize('keep_runs_results', [True, False]) @@ -407,7 +409,7 @@ def test_multitraj_targettol(self, keep_runs_results, include_no_jump=include_no_jump) assert m_res.stats['end_condition'] == "target tolerance reached" - assert m_res.num_trajectories <= 1000 + assert m_res.num_trajectories <= 500 def test_multitraj_steadystate(self): N = 5 @@ -431,15 +433,19 @@ def test_repr(self, keep_runs_results): if keep_runs_results: assert "Trajectories saved." in repr - @pytest.mark.parametrize('keep_runs_results', [True, False]) - def test_merge_result(self, keep_runs_results): + @pytest.mark.parametrize('keep_runs_results1', [True, False]) + @pytest.mark.parametrize('keep_runs_results2', [True, False]) + def test_merge_result(self, keep_runs_results1, keep_runs_results2): N = 10 opt = fill_options( - keep_runs_results=keep_runs_results, store_states=True + keep_runs_results=keep_runs_results1, store_states=True ) m_res1 = MultiTrajResult([qutip.num(10)], opt, stats={"run time": 1}) self._fill_trajectories(m_res1, N, 10, noise=0.1) + opt = fill_options( + keep_runs_results=keep_runs_results2, store_states=True + ) m_res2 = MultiTrajResult([qutip.num(10)], opt, stats={"run time": 2}) self._fill_trajectories(m_res2, N, 30, noise=0.1) @@ -456,7 +462,9 @@ def test_merge_result(self, keep_runs_results): np.ones(N), rtol=0.1 ) - assert bool(merged_res.trajectories) == keep_runs_results + assert bool(merged_res.trajectories) == ( + keep_runs_results1 and keep_runs_results2 + ) assert merged_res.stats["run time"] == 3 def _random_ensemble(self, abs_weights=True, collapse=False, trace=False, @@ -508,10 +516,7 @@ def test_merge_weights(self, abs_weights1, abs_weights2, p): merged = ensemble1.merge(ensemble2, p=p) if p is None: - p = ensemble1._num_rel_trajectories / ( - ensemble1._num_rel_trajectories + - ensemble2._num_rel_trajectories - ) + p = 0.5 np.testing.assert_almost_equal( merged.expect[0], @@ -535,10 +540,7 @@ def test_merge_mcresult(self, p): merged = ensemble1.merge(ensemble2, p=p) if p is None: - p = ensemble1._num_rel_trajectories / ( - ensemble1._num_rel_trajectories + - ensemble2._num_rel_trajectories - ) + p = 0.5 assert merged.num_trajectories == len(merged.collapse) @@ -556,10 +558,7 @@ def test_merge_nmmcresult(self, p): merged = ensemble1.merge(ensemble2, p=p) if p is None: - p = ensemble1._num_rel_trajectories / ( - ensemble1._num_rel_trajectories + - ensemble2._num_rel_trajectories - ) + p = 0.5 np.testing.assert_almost_equal( merged.trace, p * ensemble1.trace + (1 - p) * ensemble2.trace) From 491d70bb4b2a15efdc423127d19125fa8d335f5e Mon Sep 17 00:00:00 2001 From: Andrey Nikitin <143126464+pyukey@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:19:36 -0400 Subject: [PATCH 236/305] Fixed -Inf error Get the absolute of eigenvalues so scipy.entr doesn't return -Inf --- qutip/piqs/piqs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/piqs/piqs.py b/qutip/piqs/piqs.py index 73b3995581..c06ec2b145 100644 --- a/qutip/piqs/piqs.py +++ b/qutip/piqs/piqs.py @@ -268,7 +268,7 @@ def dicke_function_trace(f, rho): normalized_block = block / dj eigenvals_block = eigvalsh(normalized_block) for val in eigenvals_block: - eigenvals_degeneracy.append(val) + eigenvals_degeneracy.append(abs(val)) deg.append(dj) eigenvalue = np.array(eigenvals_degeneracy) From 51a7dd9bc4d6c8987fe125aed3a7f87d51e147b7 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 19 Jun 2024 17:14:26 -0400 Subject: [PATCH 237/305] Add type hints for creators --- qutip/core/gates.py | 67 ++++++++------- qutip/core/operators.py | 142 ++++++++++++++++++++----------- qutip/core/qobj.py | 4 +- qutip/core/states.py | 166 +++++++++++++++++++++++++++++-------- qutip/random_objects.py | 90 ++++++++++++++++---- qutip/tests/test_random.py | 6 +- qutip/typing.py | 11 +++ 7 files changed, 348 insertions(+), 138 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index 9fc1bb9c71..a80fa3f67e 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -8,6 +8,7 @@ from .dimensions import Dimensions from .. import settings from . import data as _data +from ..typing import LayerType __all__ = [ @@ -46,7 +47,7 @@ _DIMS_3_QB = Dimensions([[2, 2, 2], [2, 2, 2]]) -def cy_gate(*, dtype=None): +def cy_gate(*, dtype: LayerType = None) -> Qobj: """Controlled Y gate. Parameters @@ -69,7 +70,7 @@ def cy_gate(*, dtype=None): ).to(dtype) -def cz_gate(*, dtype=None): +def cz_gate(*, dtype: LayerType = None) -> Qobj: """Controlled Z gate. Parameters @@ -87,7 +88,7 @@ def cz_gate(*, dtype=None): return qdiags([1, 1, 1, -1], dims=_DIMS_2_QB, dtype=dtype) -def s_gate(*, dtype=None): +def s_gate(*, dtype: LayerType = None) -> Qobj: """Single-qubit rotation also called Phase gate or the Z90 gate. Parameters @@ -107,7 +108,7 @@ def s_gate(*, dtype=None): return qdiags([1, 1j], dtype=dtype) -def cs_gate(*, dtype=None): +def cs_gate(*, dtype: LayerType = None) -> Qobj: """Controlled S gate. Parameters @@ -126,7 +127,7 @@ def cs_gate(*, dtype=None): return qdiags([1, 1, 1, 1j], dims=_DIMS_2_QB, dtype=dtype) -def t_gate(*, dtype=None): +def t_gate(*, dtype: LayerType = None) -> Qobj: """Single-qubit rotation related to the S gate by the relationship S=T*T. Parameters @@ -145,7 +146,7 @@ def t_gate(*, dtype=None): return qdiags([1, np.exp(1j * np.pi / 4)], dtype=dtype) -def ct_gate(*, dtype=None): +def ct_gate(*, dtype: LayerType = None) -> Qobj: """Controlled T gate. Parameters @@ -168,7 +169,7 @@ def ct_gate(*, dtype=None): ) -def rx(phi, *, dtype=None): +def rx(phi, *, dtype: LayerType = None) -> Qobj: """Single-qubit rotation for operator sigmax with angle phi. Parameters @@ -197,7 +198,7 @@ def rx(phi, *, dtype=None): ).to(dtype) -def ry(phi, *, dtype=None): +def ry(phi, *, dtype: LayerType = None) -> Qobj: """Single-qubit rotation for operator sigmay with angle phi. Parameters @@ -226,7 +227,7 @@ def ry(phi, *, dtype=None): ).to(dtype) -def rz(phi, *, dtype=None): +def rz(phi, *, dtype: LayerType = None) -> Qobj: """Single-qubit rotation for operator sigmaz with angle phi. Parameters @@ -248,7 +249,7 @@ def rz(phi, *, dtype=None): return qdiags([np.exp(-1j * phi / 2), np.exp(1j * phi / 2)], dtype=dtype) -def sqrtnot(*, dtype=None): +def sqrtnot(*, dtype: LayerType = None) -> Qobj: """Single-qubit square root NOT gate. Parameters @@ -271,7 +272,7 @@ def sqrtnot(*, dtype=None): ).to(dtype) -def snot(*, dtype=None): +def snot(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the SNOT (Hadamard) gate. Parameters @@ -294,7 +295,7 @@ def snot(*, dtype=None): ).to(dtype) -def phasegate(theta, *, dtype=None): +def phasegate(theta: float, *, dtype: LayerType = None) -> Qobj: """ Returns quantum object representing the phase shift gate. @@ -316,7 +317,7 @@ def phasegate(theta, *, dtype=None): return qdiags([1, np.exp(1.0j * theta)], dtype=dtype) -def qrot(theta, phi, *, dtype=None): +def qrot(theta: float, phi: float, *, dtype: LayerType = None) -> Qobj: """ Single qubit rotation driving by Rabi oscillation with 0 detune. @@ -352,7 +353,7 @@ def qrot(theta, phi, *, dtype=None): # -def cphase(theta, *, dtype=None): +def cphase(theta: float, *, dtype: LayerType = None) -> Qobj: """ Returns quantum object representing the controlled phase shift gate. @@ -375,7 +376,7 @@ def cphase(theta, *, dtype=None): ) -def cnot(*, dtype=None): +def cnot(*, dtype: LayerType = None) -> Qobj: """ Quantum object representing the CNOT gate. @@ -400,7 +401,7 @@ def cnot(*, dtype=None): ).to(dtype) -def csign(*, dtype=None): +def csign(*, dtype: LayerType = None) -> Qobj: """ Quantum object representing the CSIGN gate. @@ -419,7 +420,7 @@ def csign(*, dtype=None): return cz_gate(dtype=dtype) -def berkeley(*, dtype=None): +def berkeley(*, dtype: LayerType = None) -> Qobj: """ Quantum object representing the Berkeley gate. @@ -449,7 +450,7 @@ def berkeley(*, dtype=None): ).to(dtype) -def swapalpha(alpha, *, dtype=None): +def swapalpha(alpha: float, *, dtype: LayerType = None) -> Qobj: """ Quantum object representing the SWAPalpha gate. @@ -482,7 +483,7 @@ def swapalpha(alpha, *, dtype=None): ).to(dtype) -def swap(*, dtype=None): +def swap(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the SWAP gate. Parameters @@ -506,7 +507,7 @@ def swap(*, dtype=None): ).to(dtype) -def iswap(*, dtype=None): +def iswap(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the iSWAP gate. Parameters @@ -529,7 +530,7 @@ def iswap(*, dtype=None): ).to(dtype) -def sqrtswap(*, dtype=None): +def sqrtswap(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the square root SWAP gate. Parameters @@ -560,7 +561,7 @@ def sqrtswap(*, dtype=None): ).to(dtype) -def sqrtiswap(*, dtype=None): +def sqrtiswap(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the square root iSWAP gate. Parameters @@ -590,7 +591,7 @@ def sqrtiswap(*, dtype=None): ).to(dtype) -def molmer_sorensen(theta, *, dtype=None): +def molmer_sorensen(theta: float, *, dtype: LayerType = None) -> Qobj: """ Quantum object of a Mølmer–Sørensen gate. @@ -598,8 +599,6 @@ def molmer_sorensen(theta, *, dtype=None): ---------- theta: float The duration of the interaction pulse. - N: int - Number of qubits in the system. target: int The indices of the target qubits. dtype : str or type, [keyword only] [optional] @@ -630,7 +629,7 @@ def molmer_sorensen(theta, *, dtype=None): # -def fredkin(*, dtype=None): +def fredkin(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the Fredkin gate. Parameters @@ -663,7 +662,7 @@ def fredkin(*, dtype=None): ).to(dtype) -def toffoli(*, dtype=None): +def toffoli(*, dtype: LayerType = None) -> Qobj: """Quantum object representing the Toffoli gate. Parameters @@ -701,7 +700,7 @@ def toffoli(*, dtype=None): # -def globalphase(theta, N=1, *, dtype=None): +def globalphase(theta: float, N: int = 1, *, dtype: LayerType = None) -> Qobj: """ Returns quantum object representing the global phase shift gate. @@ -710,6 +709,9 @@ def globalphase(theta, N=1, *, dtype=None): theta : float Phase rotation angle. + N : int: + Number of qubits + dtype : str or type, [keyword only] [optional] Storage representation. Any data-layer known to `qutip.data.to` is accepted. @@ -741,11 +743,14 @@ def _hamming_distance(x): return tot -def hadamard_transform(N=1, *, dtype=None): +def hadamard_transform(N: int = 1, *, dtype: LayerType = None) -> Qobj: """Quantum object representing the N-qubit Hadamard gate. Parameters ---------- + N : int: + Number of qubits + dtype : str or type, [keyword only] [optional] Storage representation. Any data-layer known to `qutip.data.to` is accepted. @@ -782,7 +787,7 @@ def _powers(op, N): yield acc -def qubit_clifford_group(*, dtype=None): +def qubit_clifford_group(*, dtype: LayerType = None) -> list[Qobj]: """ Generates the Clifford group on a single qubit, using the presentation of the group given by Ross and Selinger @@ -814,7 +819,7 @@ def qubit_clifford_group(*, dtype=None): X = sigmax() S = phasegate(np.pi / 2) - E = H * (S**3) * w**3 + E = H @ (S**3) @ w**3 # partial(reduce, mul) returns a function that takes products # of its argument, by analogy to sum. Note that by analogy, diff --git a/qutip/core/operators.py b/qutip/core/operators.py index 94e2e2bcb2..e09613f68c 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -13,23 +13,30 @@ ] import numpy as np +from typing import Literal from . import data as _data from .qobj import Qobj from .dimensions import Space from .. import settings - - -def qdiags(diagonals, offsets=None, dims=None, shape=None, *, - dtype=None): +from ..typing import DimensionLike, SpaceLike, LayerType + +def qdiags( + diagonals: np.typing.ArrayLike | list[np.typing.ArrayLike], + offsets: int | list[int] = None, + dims: DimensionLike = None, + shape: tuple[int, int] = None, + *, + dtype: LayerType = None, +) -> Qobj: """ Constructs an operator from an array of diagonals. Parameters ---------- - diagonals : sequence of array_like + diagonals : array_like or sequence of array_like Array of elements to place along the selected diagonals. - offsets : sequence of ints, optional + offsets : int or sequence of ints, optional Sequence for diagonals to be set: - k=0 main diagonal - k>0 kth upper diagonal @@ -78,7 +85,12 @@ def qdiags(diagonals, offsets=None, dims=None, shape=None, *, ) -def jmat(j, which=None, *, dtype=None): +def jmat( + j: float, + which: Literal["x", "y", "z", "+", "-"] = None, + *, + dtype: LayerType = None +) -> Qobj | tuple[Qobj]: """Higher-order spin operators: Parameters @@ -177,7 +189,7 @@ def _jz(j, *, dtype=None): # # Spin j operators: # -def spin_Jx(j, *, dtype=None): +def spin_Jx(j: float, *, dtype: LayerType = None) -> Qobj: """Spin-j x operator Parameters @@ -198,7 +210,7 @@ def spin_Jx(j, *, dtype=None): return jmat(j, 'x', dtype=dtype) -def spin_Jy(j, *, dtype=None): +def spin_Jy(j: float, *, dtype: LayerType = None) -> Qobj: """Spin-j y operator Parameters @@ -219,7 +231,7 @@ def spin_Jy(j, *, dtype=None): return jmat(j, 'y', dtype=dtype) -def spin_Jz(j, *, dtype=None): +def spin_Jz(j: float, *, dtype: LayerType = None) -> Qobj: """Spin-j z operator Parameters @@ -240,7 +252,7 @@ def spin_Jz(j, *, dtype=None): return jmat(j, 'z', dtype=dtype) -def spin_Jm(j, *, dtype=None): +def spin_Jm(j: float, *, dtype: LayerType = None) -> Qobj: """Spin-j annihilation operator Parameters @@ -261,7 +273,7 @@ def spin_Jm(j, *, dtype=None): return jmat(j, '-', dtype=dtype) -def spin_Jp(j, *, dtype=None): +def spin_Jp(j: float, *, dtype: LayerType = None) -> Qobj: """Spin-j creation operator Parameters @@ -282,7 +294,7 @@ def spin_Jp(j, *, dtype=None): return jmat(j, '+', dtype=dtype) -def spin_J_set(j, *, dtype=None): +def spin_J_set(j: float, *, dtype: LayerType = None) -> tuple[Qobj]: """Set of spin-j operators (x, y, z) Parameters @@ -296,7 +308,7 @@ def spin_J_set(j, *, dtype=None): Returns ------- - list : list of Qobj + list : tuple of Qobj list of ``qobj`` representating of the spin operator. """ @@ -318,7 +330,7 @@ def spin_J_set(j, *, dtype=None): _SIGMAZ._isunitary = True -def sigmap(*, dtype=None): +def sigmap(*, dtype: LayerType = None) -> Qobj: """Creation operator for Pauli spins. Parameters @@ -341,7 +353,7 @@ def sigmap(*, dtype=None): return _SIGMAP.to(dtype, True) -def sigmam(*, dtype=None): +def sigmam(*, dtype: LayerType = None) -> Qobj: """Annihilation operator for Pauli spins. Parameters @@ -364,7 +376,7 @@ def sigmam(*, dtype=None): return _SIGMAM.to(dtype, True) -def sigmax(*, dtype=None): +def sigmax(*, dtype: LayerType = None) -> Qobj: """Pauli spin 1/2 sigma-x operator Parameters @@ -387,7 +399,7 @@ def sigmax(*, dtype=None): return _SIGMAX.to(dtype, True) -def sigmay(*, dtype=None): +def sigmay(*, dtype: LayerType = None) -> Qobj: """Pauli spin 1/2 sigma-y operator. Parameters @@ -410,7 +422,7 @@ def sigmay(*, dtype=None): return _SIGMAY.to(dtype, True) -def sigmaz(*, dtype=None): +def sigmaz(*, dtype: LayerType = None) -> Qobj: """Pauli spin 1/2 sigma-z operator. Parameters @@ -433,7 +445,7 @@ def sigmaz(*, dtype=None): return _SIGMAZ.to(dtype, True) -def destroy(N, offset=0, *, dtype=None): +def destroy(N: int, offset: int = 0, *, dtype: LayerType = None) -> Qobj: """ Destruction (lowering) operator. @@ -472,7 +484,7 @@ def destroy(N, offset=0, *, dtype=None): return qdiags(data, 1, dtype=dtype) -def create(N, offset=0, *, dtype=None): +def create(N: int, offset: int = 0, *, dtype: LayerType = None) -> Qobj: """ Creation (raising) operator. @@ -511,7 +523,7 @@ def create(N, offset=0, *, dtype=None): return qdiags(data, -1, dtype=dtype) -def fdestroy(n_sites, site, dtype=None): +def fdestroy(n_sites: int, site, dtype: LayerType = None) -> Qobj: """ Fermionic destruction operator. We use the Jordan-Wigner transformation, @@ -529,7 +541,7 @@ def fdestroy(n_sites, site, dtype=None): n_sites : int Number of sites in Fock space. - site : int, default: 0 + site : int The site in Fock space to add a fermion to. Corresponds to j in the above JW transform. @@ -556,7 +568,7 @@ def fdestroy(n_sites, site, dtype=None): return _f_op(n_sites, site, 'destruction', dtype=dtype) -def fcreate(n_sites, site, dtype=None): +def fcreate(n_sites: int, site, dtype: LayerType = None) -> Qobj: """ Fermionic creation operator. We use the Jordan-Wigner transformation, @@ -602,7 +614,7 @@ def fcreate(n_sites, site, dtype=None): return _f_op(n_sites, site, 'creation', dtype=dtype) -def _f_op(n_sites, site, action, dtype=None): +def _f_op(n_sites, site, action, dtype: LayerType = None,): """ Makes fermionic creation and destruction operators. We use the Jordan-Wigner transformation, making use of the Jordan-Wigner ZZ..Z strings, @@ -669,7 +681,12 @@ def _f_op(n_sites, site, action, dtype=None): return out -def qzero(dimensions, dims_right=None, *, dtype=None): +def qzero( + dimensions: SpaceLike, + dims_right: SpaceLike = None, + *, + dtype: LayerType = None +) -> Qobj: """ Zero operator. @@ -710,7 +727,7 @@ def qzero(dimensions, dims_right=None, *, dtype=None): isherm=True, isunitary=False, copy=False) -def qzero_like(qobj): +def qzero_like(qobj: Qobj) -> Qobj: """ Zero operator of the same dims and type as the reference. @@ -732,7 +749,7 @@ def qzero_like(qobj): ) -def qeye(dimensions, *, dtype=None): +def qeye(dimensions: SpaceLike, *, dtype: LayerType = None) -> Qobj: """ Identity operator. @@ -782,7 +799,7 @@ def qeye(dimensions, *, dtype=None): identity = qeye -def qeye_like(qobj): +def qeye_like(qobj: Qobj) -> Qobj: """ Identity operator with the same dims and type as the reference quantum object. @@ -808,7 +825,7 @@ def qeye_like(qobj): ) -def position(N, offset=0, *, dtype=None): +def position(N: int, offset: int = 0, *, dtype: LayerType = None) -> Qobj: """ Position operator :math:`x = 1 / sqrt(2) * (a + a.dag())` @@ -838,7 +855,7 @@ def position(N, offset=0, *, dtype=None): return position.to(dtype) -def momentum(N, offset=0, *, dtype=None): +def momentum(N: int, offset: int = 0, *, dtype: LayerType = None) -> Qobj: """ Momentum operator p=-1j/sqrt(2)*(a-a.dag()) @@ -868,7 +885,7 @@ def momentum(N, offset=0, *, dtype=None): return momentum.to(dtype) -def num(N, offset=0, *, dtype=None): +def num(N: int, offset: int = 0, *, dtype: LayerType = None) -> Qobj: """ Quantum object for number operator. @@ -905,7 +922,13 @@ def num(N, offset=0, *, dtype=None): return qdiags(data, 0, dtype=dtype) -def squeeze(N, z, offset=0, *, dtype=None): +def squeeze( + N: int, + z: float, + offset: int = 0, + *, + dtype: LayerType = None, +) -> Qobj: """Single-mode squeezing operator. Parameters @@ -950,7 +973,7 @@ def squeeze(N, z, offset=0, *, dtype=None): return out -def squeezing(a1, a2, z): +def squeezing(a1: Qobj, a2: Qobj, z: float) -> Qobj: """Generalized squeezing operator. .. math:: @@ -979,7 +1002,13 @@ def squeezing(a1, a2, z): return b.expm() -def displace(N, alpha, offset=0, *, dtype=None): +def displace( + N: int, + alpha: float, + offset: int = 0, + *, + dtype: LayerType = None, +) -> Qobj: """Single-mode displacement operator. Parameters @@ -1023,7 +1052,11 @@ def displace(N, alpha, offset=0, *, dtype=None): return out -def commutator(A, B, kind="normal"): +def commutator( + A: Qobj, + B: Qobj, + kind: Literal["normal", "anti"] = "normal" +) -> Qobj: """ Return the commutator of kind `kind` (normal, anti) of the two operators A and B. @@ -1046,7 +1079,7 @@ def commutator(A, B, kind="normal"): raise TypeError("Unknown commutator kind '%s'" % kind) -def qutrit_ops(*, dtype=None): +def qutrit_ops(*, dtype: LayerType = None) -> list[Qobj]: """ Operators for a three level system (qutrit). @@ -1065,19 +1098,22 @@ def qutrit_ops(*, dtype=None): from .states import qutrit_basis dtype = dtype or settings.core["default_dtype"] or _data.CSR - out = np.empty((6,), dtype=object) + out = [] basis = qutrit_basis(dtype=dtype) for i in range(3): - out[i] = basis[i] @ basis[i].dag() - out[i].isherm = True - out[i]._isunitary = False - out[i+3] = basis[i] @ basis[(i+1)%3].dag() - out[i+3].isherm = False - out[i+3]._isunitary = False + op = basis[i] @ basis[i].dag() + op.isherm = True + op._isunitary = False + out.append(op) + for i in range(3): + op = basis[i] @ basis[(i+1)%3].dag() + op.isherm = False + op._isunitary = False + out.append(op) return out -def phase(N, phi0=0, *, dtype=None): +def phase(N: int, phi0: float = 0, *, dtype: LayerType = None) -> Qobj: """ Single-mode Pegg-Barnett phase operator. @@ -1118,7 +1154,13 @@ def phase(N, phi0=0, *, dtype=None): ).to(dtype) -def charge(Nmax, Nmin=None, frac=1, *, dtype=None): +def charge( + Nmax: int, + Nmin: int = None, + frac: float = 1, + *, + dtype: LayerType = None +) -> Qobj: """ Generate the diagonal charge operator over charge states from Nmin to Nmax. @@ -1157,7 +1199,7 @@ def charge(Nmax, Nmin=None, frac=1, *, dtype=None): return out -def tunneling(N, m=1, *, dtype=None): +def tunneling(N: int, m: int = 1, *, dtype: LayerType = None) -> Qobj: r""" Tunneling operator with elements of the form :math:`\\sum |N> Qobj: """ Quantum Fourier Transform operator. @@ -1197,7 +1239,7 @@ def qft(dimensions, *, dtype=None): ints, then the dimension is the product over this list, but the ``dims`` property of the new Qobj are set to this list. - dtype : str or type, [keyword only] [optional] + dtype : type or str, optional Storage representation. Any data-layer known to ``qutip.data.to`` is accepted. @@ -1219,7 +1261,7 @@ def qft(dimensions, *, dtype=None): return Qobj(data, isherm=False, isunitary=True, dims=dims).to(dtype) -def swap(N, M, *, dtype=None): +def swap(N: int, M: int, *, dtype: LayerType = None) -> Qobj: """ Operator that exchanges the order of tensored spaces: diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 16fe9a251e..d8c41588ec 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -14,7 +14,7 @@ from .. import __version__ from ..settings import settings from . import data as _data -from qutip.typing import LayerType +from qutip.typing import LayerType, DimensionLike from .dimensions import ( enumerate_flat, collapse_dims_super, flatten, unflatten, Dimensions ) @@ -269,7 +269,7 @@ def _initialize_data(self, arg, dims, copy): def __init__( self, arg: ArrayLike | Any = None, - dims: list[list[int]] | list[list[list[int]]] | Dimensions = None, + dims: DimensionLike = None, copy: bool = True, superrep: str = None, isherm: bool = None, diff --git a/qutip/core/states.py b/qutip/core/states.py index 2ca6754d50..f58d6b7398 100644 --- a/qutip/core/states.py +++ b/qutip/core/states.py @@ -9,10 +9,10 @@ import itertools import numbers import warnings - +from collections.abc import Iterator +from typing import Literal import numpy as np import scipy.sparse as sp -import itertools from . import data as _data from .qobj import Qobj @@ -20,6 +20,7 @@ from .tensor import tensor from .dimensions import Space from .. import settings +from ..typing import SpaceLike, LayerType def _promote_to_zero_list(arg, length): @@ -60,7 +61,13 @@ def _to_space(dimensions): return Space([dimensions]) -def basis(dimensions, n=None, offset=None, *, dtype=None): +def basis( + dimensions: SpaceLike, + n: int | list[int] = None, + offset: int | list[int] = None, + *, + dtype: LayerType = None, +) -> Qobj: """Generates the vector representation of a Fock state. Parameters @@ -162,7 +169,7 @@ def basis(dimensions, n=None, offset=None, *, dtype=None): copy=False) -def qutrit_basis(*, dtype=None): +def qutrit_basis(*, dtype: LayerType = None) -> list[Qobj]: """Basis states for a three level system (qutrit) dtype : type or str, optional @@ -176,8 +183,7 @@ def qutrit_basis(*, dtype=None): """ dtype = dtype or settings.core["default_dtype"] or _data.Dense - out = np.empty((3,), dtype=object) - out[:] = [ + out = [ basis(3, 0, dtype=dtype), basis(3, 1, dtype=dtype), basis(3, 2, dtype=dtype), @@ -188,7 +194,14 @@ def qutrit_basis(*, dtype=None): _COHERENT_METHODS = ('operator', 'analytic') -def coherent(N, alpha, offset=0, method=None, *, dtype=None): +def coherent( + N: int, + alpha: float, + offset: int = 0, + method: str = None, + *, + dtype: LayerType = None, +) -> Qobj: """Generates a coherent state with eigenvalue alpha. Constructed using displacement operator on vacuum state. @@ -255,7 +268,7 @@ def coherent(N, alpha, offset=0, method=None, *, dtype=None): "The method 'operator' does not support offset != 0. Please" " select another method or set the offset to zero." ) - return (displace(N, alpha, dtype=dtype) * basis(N, 0)).to(dtype) + return (displace(N, alpha, dtype=dtype) @ basis(N, 0)).to(dtype) elif method == "analytic": sqrtn = np.sqrt(np.arange(offset, offset+N, dtype=complex)) @@ -273,7 +286,14 @@ def coherent(N, alpha, offset=0, method=None, *, dtype=None): ) -def coherent_dm(N, alpha, offset=0, method='operator', *, dtype=None): +def coherent_dm( + N: int, + alpha: float, + offset: int = 0, + method: str = None, + *, + dtype: LayerType = None, +) -> Qobj: """Density matrix representation of a coherent state. Constructed via outer product of :func:`coherent` @@ -332,7 +352,13 @@ def coherent_dm(N, alpha, offset=0, method='operator', *, dtype=None): ).proj().to(dtype) -def fock_dm(dimensions, n=None, offset=None, *, dtype=None): +def fock_dm( + dimensions: int | list[int] | Space, + n: int | list[int] = None, + offset: int | list[int] = None, + *, + dtype: LayerType = None, +) -> Qobj: """Density matrix representation of a Fock state Constructed via outer product of :func:`basis`. @@ -377,7 +403,13 @@ def fock_dm(dimensions, n=None, offset=None, *, dtype=None): return basis(dimensions, n, offset=offset, dtype=dtype).proj().to(dtype) -def fock(dimensions, n=None, offset=None, *, dtype=None): +def fock( + dimensions: SpaceLike, + n: int | list[int] = None, + offset: int | list[int] = None, + *, + dtype: LayerType = None, +) -> Qobj: """Bosonic Fock (number) state. Same as :func:`basis`. @@ -420,7 +452,13 @@ def fock(dimensions, n=None, offset=None, *, dtype=None): return basis(dimensions, n, offset=offset, dtype=dtype) -def thermal_dm(N, n, method='operator', *, dtype=None): +def thermal_dm( + N: int, + n: float, + method: str = 'operator', + *, + dtype: LayerType = None, +) -> Qobj: """Density matrix for a thermal state of n particles Parameters @@ -499,7 +537,11 @@ def thermal_dm(N, n, method='operator', *, dtype=None): return out -def maximally_mixed_dm(dimensions, *, dtype=None): +def maximally_mixed_dm( + dimensions: SpaceLike, + *, + dtype: LayerType = None +) -> Qobj: """ Returns the maximally mixed density matrix for a Hilbert space of dimension N. @@ -528,7 +570,7 @@ def maximally_mixed_dm(dimensions, *, dtype=None): isherm=True, isunitary=(N == 1), copy=False) -def ket2dm(Q): +def ket2dm(Q: Qobj) -> Qobj: """ Takes input ket or bra vector and returns density matrix formed by outer product. This is completely identical to calling ``Q.proj()``. @@ -560,7 +602,14 @@ def ket2dm(Q): raise TypeError("Input is not a ket or bra vector.") -def projection(dimensions, n, m, offset=None, *, dtype=None): +def projection( + dimensions: int | list[int], + n: int | list[int], + m: int | list[int], + offset: int | list[int] = None, + *, + dtype: LayerType = None, +) -> Qobj: r""" The projection operator that projects state :math:`\lvert m\rangle` on state :math:`\lvert n\rangle`. @@ -594,7 +643,7 @@ def projection(dimensions, n, m, offset=None, *, dtype=None): ).to(dtype) -def qstate(string, *, dtype=None): +def qstate(string: str, *, dtype: LayerType = None) -> Qobj: r"""Creates a tensor product for a set of qubits in either the 'up' :math:`\lvert0\rangle` or 'down' :math:`\lvert1\rangle` state. @@ -652,14 +701,19 @@ def qstate(string, *, dtype=None): } -def _character_to_qudit(x): +def _character_to_qudit(x: int | str) -> int: """ Converts a character representing a one-particle state into int. """ return _qubit_dict[x] if x in _qubit_dict else int(x) -def ket(seq, dim=2, *, dtype=None): +def ket( + seq: list[int | str] | str, + dim: int | list[int] = 2, + *, + dtype: LayerType = None, +) -> Qobj: """ Produces a multiparticle ket state for a list or string, where each element stands for state of the respective particle. @@ -743,7 +797,12 @@ def ket(seq, dim=2, *, dtype=None): return basis(dim, ns, dtype=dtype) -def bra(seq, dim=2, *, dtype=None): +def bra( + seq: list[int | str] | str, + dim: int | list[int] = 2, + *, + dtype: LayerType = None, +) -> Qobj: """ Produces a multiparticle bra state for a list or string, where each element stands for state of the respective particle. @@ -801,7 +860,10 @@ def bra(seq, dim=2, *, dtype=None): return ket(seq, dim=dim, dtype=dtype).dag() -def state_number_enumerate(dims, excitations=None): +def state_number_enumerate( + dims: list[int], + excitations: int = None +) -> Iterator[tuple]: """ An iterator that enumerates all the state number tuples (quantum numbers of the form (n1, n2, n3, ...)) for a system with dimensions given by dims. @@ -817,7 +879,7 @@ def state_number_enumerate(dims, excitations=None): Parameters ---------- - dims : list or array + dims : list The quantum state dimensions array, as it would appear in a Qobj. excitations : integer, optional @@ -859,7 +921,10 @@ def state_number_enumerate(dims, excitations=None): state = state[:idx] + (state[idx]+1, 0) + state[idx+2:] -def state_number_index(dims, state): +def state_number_index( + dims: list[int], + state: list[int], +) -> int: """ Return the index of a quantum state corresponding to state, given a system with dimensions given by dims. @@ -871,7 +936,7 @@ def state_number_index(dims, state): Parameters ---------- - dims : list or array + dims : list The quantum state dimensions array, as it would appear in a Qobj. state : list @@ -887,7 +952,10 @@ def state_number_index(dims, state): return np.ravel_multi_index(state, dims) -def state_index_number(dims, index): +def state_index_number( + dims: list[int], + index: int, +) -> tuple: """ Return a quantum number representation given a state index, for a system of composite structure defined by dims. @@ -915,7 +983,12 @@ def state_index_number(dims, index): return np.unravel_index(index, dims) -def state_number_qobj(dims, state, *, dtype=None): +def state_number_qobj( + dims: SpaceLike, + state: int | list[int] = None, + *, + dtype: LayerType = None, +) -> Qobj: """ Return a Qobj representation of a quantum state specified by the state array `state`. @@ -961,7 +1034,13 @@ def state_number_qobj(dims, state, *, dtype=None): return basis(dims, state, dtype=dtype) -def phase_basis(N, m, phi0=0, *, dtype=None): +def phase_basis( + N: int, + m: int, + phi0: float = 0, + *, + dtype: LayerType = None, +) -> Qobj: """ Basis vector for the mth phase of the Pegg-Barnett phase operator. @@ -999,7 +1078,7 @@ def phase_basis(N, m, phi0=0, *, dtype=None): return Qobj(data, dims=[[N], [1]], copy=False).to(dtype) -def zero_ket(dimensions, *, dtype=None): +def zero_ket(dimensions: SpaceLike, *, dtype: LayerType = None) -> Qobj: """ Creates the zero ket vector with shape Nx1 and dimensions `dims`. @@ -1026,7 +1105,13 @@ def zero_ket(dimensions, *, dtype=None): dims=[dimensions, dimensions.scalar_like()], copy=False) -def spin_state(j, m, type='ket', *, dtype=None): +def spin_state( + j: float, + m: float, + type: Literal["ket", "bra", "dm"] = "ket", + *, + dtype: LayerType = None, +) -> Qobj: r"""Generates the spin state :math:`\lvert j, m\rangle`, i.e. the eigenstate of the spin-j Sz operator with eigenvalue m. @@ -1035,7 +1120,7 @@ def spin_state(j, m, type='ket', *, dtype=None): j : float The spin of the state (). - m : int + m : float Eigenvalue of the spin-j Sz operator. type : string {'ket', 'bra', 'dm'}, default: 'ket' @@ -1063,7 +1148,14 @@ def spin_state(j, m, type='ket', *, dtype=None): raise ValueError(f"Invalid value keyword argument type='{type}'") -def spin_coherent(j, theta, phi, type='ket', *, dtype=None): +def spin_coherent( + j: float, + theta: float, + phi: float, + type: Literal["ket", "bra", "dm"] = "ket", + *, + dtype: LayerType = None, +) -> Qobj: r"""Generate the coherent spin state :math:`\lvert \theta, \phi\rangle`. Parameters @@ -1112,7 +1204,11 @@ def spin_coherent(j, theta, phi, type='ket', *, dtype=None): '11': np.sqrt(0.5) * (basis([2, 2], [0, 1]) - basis([2, 2], [1, 0])), } -def bell_state(state='00', *, dtype=None): +def bell_state( + state: Literal["00", "01", "10", "11"] = "00", + *, + dtype: LayerType = None, +) -> Qobj: r""" Returns the selected Bell state: @@ -1148,7 +1244,7 @@ def bell_state(state='00', *, dtype=None): return _BELL_STATES[state].copy().to(dtype) -def singlet_state(*, dtype=None): +def singlet_state(*, dtype: LayerType = None) -> Qobj: r""" Returns the two particle singlet-state: @@ -1173,7 +1269,7 @@ def singlet_state(*, dtype=None): return bell_state('11').to(dtype) -def triplet_states(*, dtype=None): +def triplet_states(*, dtype: LayerType = None) -> list[Qobj]: r""" Returns a list of the two particle triplet-states: @@ -1207,7 +1303,7 @@ def triplet_states(*, dtype=None): ] -def w_state(N_qubit, *, dtype=None): +def w_state(N_qubit: int, *, dtype: LayerType = None) -> Qobj: """ Returns the N-qubit W-state: ``[ |100..0> + |010..0> + |001..0> + ... |000..1> ] / sqrt(n)`` @@ -1236,7 +1332,7 @@ def w_state(N_qubit, *, dtype=None): return (np.sqrt(1 / N_qubit) * state).to(dtype) -def ghz_state(N_qubit, *, dtype=None): +def ghz_state(N_qubit: int, *, dtype: LayerType = None) -> Qobj: """ Returns the N-qubit GHZ-state: ``[ |00...00> + |11...11> ] / sqrt(2)`` diff --git a/qutip/random_objects.py b/qutip/random_objects.py index 545c9639b8..afba5c9a4a 100644 --- a/qutip/random_objects.py +++ b/qutip/random_objects.py @@ -19,12 +19,14 @@ from numpy.random import Generator, SeedSequence, default_rng import scipy.linalg import scipy.sparse as sp +from typing import Literal, Sequence from . import (Qobj, create, destroy, jmat, basis, to_super, to_choi, to_chi, to_kraus, to_stinespring) from .core import data as _data -from .core.dimensions import flatten, Dimensions +from .core.dimensions import flatten, Dimensions, Space from . import settings +from .typing import SpaceLike, LayerType _RAND = default_rng() @@ -52,8 +54,10 @@ def _implicit_tensor_dimensions(dimensions, superoper=False): dimensions : list Dimension list in the form required by ``Qobj`` creation. """ + if isinstance(dimensions, Space): + pass if not isinstance(dimensions, list): - dimensions = [dimensions] + dimensions = Space(dimensions) flat = flatten(dimensions) if not all(isinstance(x, numbers.Integral) and x >= 0 for x in flat): raise ValueError("All dimensions must be integers >= 0") @@ -210,8 +214,15 @@ def _merge_shuffle_blocks(blocks, generator): return _data.create(matrix, copy=False) -def rand_herm(dimensions, density=0.30, distribution="fill", *, - eigenvalues=(), seed=None, dtype=None): +def rand_herm( + dimensions: SpaceLike, + density: float = 0.30, + distribution: Literal["fill", "pos_def", "eigen"] = "fill", + *, + eigenvalues: Sequence[float] = (), + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): """Creates a random sparse Hermitian quantum object. Parameters @@ -335,8 +346,14 @@ def _rand_herm_dense(N, density, pos_def, generator): return _data.create(M) -def rand_unitary(dimensions, density=1, distribution="haar", *, - seed=None, dtype=None): +def rand_unitary( + dimensions: SpaceLike, + density: float = 1, + distribution: Literal["haar", "exp"] = "haar", + *, + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): r"""Creates a random sparse unitary quantum object. Parameters @@ -438,8 +455,14 @@ def _rand_unitary_haar(N, generator): return Q * Lambda -def rand_ket(dimensions, density=1, distribution="haar", *, - seed=None, dtype=None): +def rand_ket( + dimensions: SpaceLike, + density: float = 1, + distribution: Literal["haar", "fill"] = "haar", + *, + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): """Creates a random ket vector. Parameters @@ -501,9 +524,17 @@ def rand_ket(dimensions, density=1, distribution="haar", *, return ket.to(dtype) -def rand_dm(dimensions, density=0.75, distribution="ginibre", *, - eigenvalues=(), rank=None, seed=None, - dtype=None): +def rand_dm( + dimensions: SpaceLike, + density: float = 0.75, + distribution: Literal["ginibre", "hs", "pure", "eigen", "uniform"] \ + = "ginibre", + *, + eigenvalues: Sequence[float] = (), + rank: int = None, + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): r"""Creates a random density matrix of the desired dimensions. Parameters @@ -631,7 +662,12 @@ def _rand_dm_ginibre(N, rank, generator): return rho -def rand_kraus_map(dimensions, *, seed=None, dtype=None): +def rand_kraus_map( + dimensions: SpaceLike, + *, + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): """ Creates a random CPTP map on an N-dimensional Hilbert space in Kraus form. @@ -671,7 +707,13 @@ def rand_kraus_map(dimensions, *, seed=None, dtype=None): return [Qobj(x, dims=dims, copy=False).to(dtype) for x in oper_list] -def rand_super(dimensions, *, superrep="super", seed=None, dtype=None): +def rand_super( + dimensions: SpaceLike, + *, + superrep: Literal["super", "choi", "chi"] = "super", + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): """ Returns a randomly drawn superoperator acting on operators acting on N dimensions. @@ -712,9 +754,15 @@ def rand_super(dimensions, *, superrep="super", seed=None, dtype=None): return out -def rand_super_bcsz(dimensions, enforce_tp=True, rank=None, *, - superrep="super", seed=None, - dtype=None): +def rand_super_bcsz( + dimensions: SpaceLike, + enforce_tp: bool = True, + rank: int = None, + *, + superrep: Literal["super", "choi", "chi"] = "super", + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): """ Returns a random superoperator drawn from the Bruzda et al ensemble for CPTP maps [BCSZ08]_. Note that due to @@ -816,8 +864,14 @@ def rand_super_bcsz(dimensions, enforce_tp=True, rank=None, *, return out -def rand_stochastic(dimensions, density=0.75, kind='left', - *, seed=None, dtype=None): +def rand_stochastic( + dimensions: SpaceLike, + density: float = 0.75, + kind: Literal["left", "right"] = "left", + *, + seed: int | SeedSequence | Generator = None, + dtype: LayerType = None, +): """Generates a random stochastic matrix. Parameters diff --git a/qutip/tests/test_random.py b/qutip/tests/test_random.py index 085e940448..3bd44be539 100644 --- a/qutip/tests/test_random.py +++ b/qutip/tests/test_random.py @@ -6,6 +6,7 @@ from qutip import qeye, num, to_kraus, kraus_to_choi, CoreOptions, Qobj from qutip import data as _data +from qutip.dimensions import Space from qutip.random_objects import ( rand_herm, rand_unitary, @@ -22,8 +23,9 @@ 12, [8], [2, 2, 3], - [[2], [2]] -], ids=["int", "list", "tensor", "super"]) + [[2], [2]], + Space(3), +], ids=["int", "list", "tensor", "super", "Space"]) def dimensions(request): return request.param diff --git a/qutip/typing.py b/qutip/typing.py index 95009a3bf8..50f7f1fafa 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -35,3 +35,14 @@ def __call__(self, t: Real, **kwargs) -> Number: LayerType = Union[str, type] + + +SpaceLike = Union[int, list[int], list[list[int]], "Space"] + + +DimensionLike = Union[ + list[list[int], list[int]], + list[list[list[int]], list[list[int]]], + list["Space", "Space"], + "Dimensions", +] From 5bf12a5a501f431a65b18f5a98773a83ef6eb7b6 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 20 Jun 2024 10:54:23 -0400 Subject: [PATCH 238/305] Install optionals dep with runtime numpy version --- .github/workflows/tests.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b80c6a19e4..55d6693be4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -140,7 +140,6 @@ jobs: run: | # Install the extra requirement python -m pip install pytest>=5.2 pytest-rerunfailures # tests - python -m pip install matplotlib>=1.2.1 # graphics python -m pip install ipython # ipython python -m pip install loky tqdm # extras python -m pip install "coverage${{ matrix.coverage-requirement }}" chardet @@ -158,9 +157,6 @@ jobs: else conda install nomkl "numpy${{ matrix.numpy-build }}" "scipy${{ matrix.scipy-requirement }}" fi - if [[ -n "${{ matrix.semidefinite }}" ]]; then - python -m pip install cvxpy>=1.0 cvxopt - fi if [[ -n "${{ matrix.conda-extra-pkgs }}" ]]; then conda install "${{ matrix.conda-extra-pkgs }}" fi @@ -189,7 +185,10 @@ jobs: else conda install nomkl "numpy${{ matrix.numpy-requirement }}" fi - + if [[ -n "${{ matrix.semidefinite }}" ]]; then + python -m pip install cvxpy>=1.0 cvxopt + fi + python -m pip install matplotlib>=1.2.1 # graphics - name: Package information run: | From 7b3ecf1055962050ba220e907c7b41fc97e2081a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 20 Jun 2024 15:05:26 -0400 Subject: [PATCH 239/305] Update changelog --- doc/changelog.rst | 26 ++++++++++++++++++++++++++ doc/changes/2400.bugfix | 3 --- doc/changes/2421.misc | 1 - doc/changes/2441.doc | 1 - doc/changes/2443.bugfix | 1 - doc/changes/2454.bugfix | 1 - doc/changes/2457.misc | 1 - 7 files changed, 26 insertions(+), 8 deletions(-) delete mode 100644 doc/changes/2400.bugfix delete mode 100644 doc/changes/2421.misc delete mode 100644 doc/changes/2441.doc delete mode 100644 doc/changes/2443.bugfix delete mode 100644 doc/changes/2454.bugfix delete mode 100644 doc/changes/2457.misc diff --git a/doc/changelog.rst b/doc/changelog.rst index 1678070584..b71f42edc1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,6 +6,32 @@ Change Log .. towncrier release notes start +QuTiP 5.0.3 (2024-06-20) +======================== + +Bug Fixes +--------- + +- Bug Fix in Process Matrix Rendering + + Resolved a rendering issue in the process matrix visualization. Previously, the code did not utilize matplotlib's built-in z-sorting mechanism. Experiments with various z-sort configurations (min, max, average) yielded inconsistent results across different charts. The solution was inspired by a Stack Overflow discussion (https://stackoverflow.com/questions/18602660/matplotlib-bar3d-clipping-problems). By adjusting the calculation of camera coordinates and incorporating minor modifications from the suggested approach, the rendering issue has been successfully addressed. (#2400) +- Fix steadystate permutation being reversed. (#2443) +- Add parallelizing support for `vernN` methods with `mcsolve`. (#2454) + + +Documentation +------------- + +- Added `qutip.core.gates` to apidoc/functions.rst and a Gates section to guide-states.rst. (#2441) + + +Miscellaneous +------------- + +- Add support for numpy 2 (#2421) +- Add numpy 2 support (#2457) + + QuTiP 5.0.2 (2024-05-16) ======================== diff --git a/doc/changes/2400.bugfix b/doc/changes/2400.bugfix deleted file mode 100644 index 9c850f9fb7..0000000000 --- a/doc/changes/2400.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Bug Fix in Process Matrix Rendering - -Resolved a rendering issue in the process matrix visualization. Previously, the code did not utilize matplotlib's built-in z-sorting mechanism. Experiments with various z-sort configurations (min, max, average) yielded inconsistent results across different charts. The solution was inspired by a Stack Overflow discussion (https://stackoverflow.com/questions/18602660/matplotlib-bar3d-clipping-problems). By adjusting the calculation of camera coordinates and incorporating minor modifications from the suggested approach, the rendering issue has been successfully addressed. \ No newline at end of file diff --git a/doc/changes/2421.misc b/doc/changes/2421.misc deleted file mode 100644 index 0ebb145cf5..0000000000 --- a/doc/changes/2421.misc +++ /dev/null @@ -1 +0,0 @@ -Add support for numpy 2 diff --git a/doc/changes/2441.doc b/doc/changes/2441.doc deleted file mode 100644 index 981bf2660c..0000000000 --- a/doc/changes/2441.doc +++ /dev/null @@ -1 +0,0 @@ -Added `qutip.core.gates` to apidoc/functions.rst and a Gates section to guide-states.rst. diff --git a/doc/changes/2443.bugfix b/doc/changes/2443.bugfix deleted file mode 100644 index 5e08a0dd71..0000000000 --- a/doc/changes/2443.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix steadystate permutation being reversed. diff --git a/doc/changes/2454.bugfix b/doc/changes/2454.bugfix deleted file mode 100644 index be2b44aa32..0000000000 --- a/doc/changes/2454.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add parallelizing support for `vernN` methods with `mcsolve`. \ No newline at end of file diff --git a/doc/changes/2457.misc b/doc/changes/2457.misc deleted file mode 100644 index 8ab04dd93a..0000000000 --- a/doc/changes/2457.misc +++ /dev/null @@ -1 +0,0 @@ -Add numpy 2 support From 2ff5f14471a379e93c1e4547be62549515e9e173 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 20 Jun 2024 15:11:40 -0400 Subject: [PATCH 240/305] Add names to changelog --- doc/changelog.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index b71f42edc1..10b0a66612 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -12,24 +12,21 @@ QuTiP 5.0.3 (2024-06-20) Bug Fixes --------- -- Bug Fix in Process Matrix Rendering - - Resolved a rendering issue in the process matrix visualization. Previously, the code did not utilize matplotlib's built-in z-sorting mechanism. Experiments with various z-sort configurations (min, max, average) yielded inconsistent results across different charts. The solution was inspired by a Stack Overflow discussion (https://stackoverflow.com/questions/18602660/matplotlib-bar3d-clipping-problems). By adjusting the calculation of camera coordinates and incorporating minor modifications from the suggested approach, the rendering issue has been successfully addressed. (#2400) +- Bug Fix in Process Matrix Rendering. (#2400, by Anush Venkatakrishnan) - Fix steadystate permutation being reversed. (#2443) -- Add parallelizing support for `vernN` methods with `mcsolve`. (#2454) +- Add parallelizing support for `vernN` methods with `mcsolve`. (#2454 by Utkarsh) Documentation ------------- -- Added `qutip.core.gates` to apidoc/functions.rst and a Gates section to guide-states.rst. (#2441) +- Added `qutip.core.gates` to apidoc/functions.rst and a Gates section to guide-states.rst. (#2441, by alan-nala) Miscellaneous ------------- -- Add support for numpy 2 (#2421) -- Add numpy 2 support (#2457) +- Add support for numpy 2 (#2421, #2457) QuTiP 5.0.2 (2024-05-16) From b8563ef1e6dd22a442393d29e79bb769cc18e973 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 20 Jun 2024 18:51:16 -0400 Subject: [PATCH 241/305] Tests random tests space input --- qutip/core/gates.py | 2 +- qutip/random_objects.py | 4 ++-- qutip/tests/test_random.py | 18 ++++++++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index a80fa3f67e..3be5c53172 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -819,7 +819,7 @@ def qubit_clifford_group(*, dtype: LayerType = None) -> list[Qobj]: X = sigmax() S = phasegate(np.pi / 2) - E = H @ (S**3) @ w**3 + E = H @ (S**3) * w**3 # partial(reduce, mul) returns a function that takes products # of its argument, by analogy to sum. Note that by analogy, diff --git a/qutip/random_objects.py b/qutip/random_objects.py index afba5c9a4a..83f1082476 100644 --- a/qutip/random_objects.py +++ b/qutip/random_objects.py @@ -55,9 +55,9 @@ def _implicit_tensor_dimensions(dimensions, superoper=False): Dimension list in the form required by ``Qobj`` creation. """ if isinstance(dimensions, Space): - pass + dimensions = dimensions.as_list() if not isinstance(dimensions, list): - dimensions = Space(dimensions) + dimensions = [dimensions] flat = flatten(dimensions) if not all(isinstance(x, numbers.Integral) and x >= 0 for x in flat): raise ValueError("All dimensions must be integers >= 0") diff --git a/qutip/tests/test_random.py b/qutip/tests/test_random.py index 3bd44be539..d5a860d79d 100644 --- a/qutip/tests/test_random.py +++ b/qutip/tests/test_random.py @@ -6,7 +6,7 @@ from qutip import qeye, num, to_kraus, kraus_to_choi, CoreOptions, Qobj from qutip import data as _data -from qutip.dimensions import Space +from qutip.core.dimensions import Space from qutip.random_objects import ( rand_herm, rand_unitary, @@ -47,6 +47,8 @@ def _assert_density(qobj, density): def _assert_metadata(random_qobj, dims, dtype=None, super=False, ket=False): if isinstance(dims, int): dims = [dims] + elif isinstance(dims, Space): + dims = dims.as_list() N = np.prod(dims) if super and not isinstance(dims[0], list): target_dims_0 = [dims, dims] @@ -97,7 +99,10 @@ def test_rand_herm_Eigs(dimensions, density): """ Random Qobjs: Hermitian matrix - Eigs given """ - N = np.prod(dimensions) + if isinstance(dimensions, Space): + N = dimensions.size + else: + N = np.prod(dimensions) eigs = np.random.random(N) eigs /= np.sum(eigs) eigs.sort() @@ -143,8 +148,11 @@ def test_rand_dm(dimensions, kw, dtype, distribution): """ Random Qobjs: Density matrix """ - N = np.prod(dimensions) - print(N, kw) + if isinstance(dimensions, Space): + N = dimensions.size + else: + N = np.prod(dimensions) + if "eigenvalues" in kw: eigs = np.random.random(N) eigs /= np.sum(eigs) @@ -240,6 +248,8 @@ def test_rand_super_bcsz(dimensions, dtype, rank, superrep): random_qobj = rand_super_bcsz(dimensions, rank=rank, dtype=dtype, superrep=superrep) + if isinstance(dimensions, Space): + dimensions = dimensions.as_list() assert random_qobj.issuper with CoreOptions(atol=1e-9): assert random_qobj.iscptp From 12ba364bc0f4ee72bbb3d9defd9f19013caf500d Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 21 Jun 2024 08:39:37 -0400 Subject: [PATCH 242/305] Add summary to changelog --- doc/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 10b0a66612..1a2f2b1337 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -9,6 +9,8 @@ Change Log QuTiP 5.0.3 (2024-06-20) ======================== +Micro release to add support for numpy 2. + Bug Fixes --------- From d96a2140cc6848f61e2b2a8b1e93426d9626cec3 Mon Sep 17 00:00:00 2001 From: Andrey Nikitin <143126464+pyukey@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:11:58 -0400 Subject: [PATCH 243/305] 2466.bugfix documentation --- doc/changes/2466.bugfix | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 doc/changes/2466.bugfix diff --git a/doc/changes/2466.bugfix b/doc/changes/2466.bugfix new file mode 100644 index 0000000000..cea72fee26 --- /dev/null +++ b/doc/changes/2466.bugfix @@ -0,0 +1,4 @@ +Fixed issue #1919 +Dicke density state matrix should only have positive eigenvalues, but rounding errors causes eigenvalues of 0 to isntead be represented as a very small negative number. +This causes dicke_trace_function to return -Inf, since scipy.special.entr returns -Inf for any values < 0. +This issue was fixed by simply taking the absolute value of the eigenvalues in dicke_trace_function. \ No newline at end of file From cdc23bf6f1f3b69a2a37d4ca511edafa6e431e81 Mon Sep 17 00:00:00 2001 From: Andrey Nikitin <143126464+pyukey@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:14:55 -0400 Subject: [PATCH 244/305] Typo in 2466.bugfix --- doc/changes/2466.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/2466.bugfix b/doc/changes/2466.bugfix index cea72fee26..2bba02592e 100644 --- a/doc/changes/2466.bugfix +++ b/doc/changes/2466.bugfix @@ -1,4 +1,4 @@ Fixed issue #1919 -Dicke density state matrix should only have positive eigenvalues, but rounding errors causes eigenvalues of 0 to isntead be represented as a very small negative number. +Dicke density state matrix should only have positive eigenvalues, but rounding errors causes eigenvalues of 0 to instead be represented as a very small negative number. This causes dicke_trace_function to return -Inf, since scipy.special.entr returns -Inf for any values < 0. This issue was fixed by simply taking the absolute value of the eigenvalues in dicke_trace_function. \ No newline at end of file From 42f500b4a11e03089cc21f7998c2ffa0118043ab Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:04:04 +0800 Subject: [PATCH 245/305] Fix Bloch.add_states color property ndim --- qutip/bloch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/bloch.py b/qutip/bloch.py index 6a7e38a687..31f23cc30f 100644 --- a/qutip/bloch.py +++ b/qutip/bloch.py @@ -393,12 +393,12 @@ def add_states(self, state, kind='vector', colors=None, alpha=1.0): if kind == 'vector': if colors is not None: - self.add_vectors(vec, colors=colors[k], alpha=alpha) + self.add_vectors(vec, colors=[colors[k]], alpha=alpha) else: self.add_vectors(vec) elif kind == 'point': if colors is not None: - self.add_points(vec, colors=colors[k], alpha=alpha) + self.add_points(vec, colors=[colors[k]], alpha=alpha) else: self.add_points(vec) From 2e6dcec5915b0f035c9fc6214858936f7234b080 Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Mon, 3 Jun 2024 14:07:20 +0800 Subject: [PATCH 246/305] Bloch guide add `add_line` and `add_arc` --- doc/guide/guide-bloch.rst | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/guide/guide-bloch.rst b/doc/guide/guide-bloch.rst index 8ca7da8a8b..c6750e0ea7 100644 --- a/doc/guide/guide-bloch.rst +++ b/doc/guide/guide-bloch.rst @@ -29,7 +29,7 @@ Before getting into the details of these objects, we can simply plot the blank B .. plot:: :context: - b.make_sphere() + b.render() In addition to the ``show`` command, see the API documentation for :class:`~qutip.bloch.Bloch` for a full list of other available functions. As an example, we can add a single data point: @@ -109,6 +109,15 @@ a similar method works for adding vectors: b.add_vectors(vec) b.render() +You can also add lines and arcs: + +.. plot:: + :context: close-figs + + b.add_line(x, y) + b.add_arc(y, z) + b.render() + Adding multiple points to the Bloch sphere works slightly differently than adding multiple states or vectors. For example, lets add a set of 20 points around the equator (after calling `clear()`): .. plot:: @@ -332,7 +341,7 @@ The code to directly generate an mp4 movie of the Qubit decay is as follows :: sphere.clear() sphere.add_vectors([np.sin(theta), 0, np.cos(theta)], ["r"]) sphere.add_points([sx[:i+1], sy[:i+1], sz[:i+1]]) - sphere.make_sphere() + sphere.render() return ax ani = animation.FuncAnimation(fig, animate, np.arange(len(sx)), blit=False, repeat=False) From 6f3dd1b1020839b40c05f8e060c728511585173d Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Mon, 3 Jun 2024 22:16:04 +0800 Subject: [PATCH 247/305] Add Towncrier entry --- doc/changes/2445.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/changes/2445.bugfix diff --git a/doc/changes/2445.bugfix b/doc/changes/2445.bugfix new file mode 100644 index 0000000000..2b947b0a17 --- /dev/null +++ b/doc/changes/2445.bugfix @@ -0,0 +1,2 @@ +Fix a dimension problem for the argument color of Bloch.add_states +Add Bloch.add_arc and Bloch.add_line in the guide on Bloch class \ No newline at end of file From 23a98974bb2a35f58a52c7912a3d016b7c736cf2 Mon Sep 17 00:00:00 2001 From: PositroniumJS <150566116+PositroniumJS@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:36:34 +0800 Subject: [PATCH 248/305] Clean-up the code in Bloch.add_states --- doc/changes/2445.bugfix | 1 + qutip/bloch.py | 46 +++++++++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/doc/changes/2445.bugfix b/doc/changes/2445.bugfix index 2b947b0a17..569c5c556d 100644 --- a/doc/changes/2445.bugfix +++ b/doc/changes/2445.bugfix @@ -1,2 +1,3 @@ Fix a dimension problem for the argument color of Bloch.add_states +Clean-up of the code in Bloch.add_state Add Bloch.add_arc and Bloch.add_line in the guide on Bloch class \ No newline at end of file diff --git a/qutip/bloch.py b/qutip/bloch.py index 31f23cc30f..7c1822609c 100644 --- a/qutip/bloch.py +++ b/qutip/bloch.py @@ -1,6 +1,7 @@ __all__ = ['Bloch'] import os +from typing import Literal import numpy as np from numpy import (outer, cos, sin, ones) @@ -314,7 +315,8 @@ def clear(self): self._lines = [] self._arcs = [] - def add_points(self, points, meth='s', colors=None, alpha=1.0): + def add_points(self, points, meth: Literal['s', 'm', 'l'] = 's', + colors=None, alpha=1.0): """Add a list of data points to bloch sphere. Parameters @@ -364,13 +366,14 @@ def add_points(self, points, meth='s', colors=None, alpha=1.0): self.point_alpha.append(alpha) self._inner_point_color.append(colors) - def add_states(self, state, kind='vector', colors=None, alpha=1.0): + def add_states(self, state, kind: Literal['vector', 'point'] = 'vector', + colors=None, alpha=1.0): """Add a state vector Qobj to Bloch sphere. Parameters ---------- - state : :obj:`.Qobj` - Input state vector. + state : :obj:`.Qobj` or array_like + Input state vector or list. kind : {'vector', 'point'} Type of object to plot. @@ -381,10 +384,27 @@ def add_states(self, state, kind='vector', colors=None, alpha=1.0): alpha : float, default=1. Transparency value for the vectors. Values between 0 and 1. """ - if isinstance(state, Qobj): - state = [state] - if not isinstance(colors, (list, np.ndarray)) and colors is not None: - colors = [colors] + state = np.asarray(state) + + if state.ndim == 0: + state = state[np.newaxis] + + if state.ndim != 1: + raise ValueError("The included states are not valid. " + "State should be a Qobj or a list of Qobj.") + + if colors is not None: + colors = np.asarray(colors) + + if colors.ndim == 0: + colors = colors[np.newaxis] + + if colors.shape != state.shape: + raise ValueError("The included colors are not valid. " + "colors must be equivalent to a 1D array " + "with the same size as the number of states.") + else: + colors = np.array([None] * state.size) for k, st in enumerate(state): vec = [expect(sigmax(), st), @@ -392,15 +412,9 @@ def add_states(self, state, kind='vector', colors=None, alpha=1.0): expect(sigmaz(), st)] if kind == 'vector': - if colors is not None: - self.add_vectors(vec, colors=[colors[k]], alpha=alpha) - else: - self.add_vectors(vec) + self.add_vectors(vec, colors=[colors[k]], alpha=alpha) elif kind == 'point': - if colors is not None: - self.add_points(vec, colors=[colors[k]], alpha=alpha) - else: - self.add_points(vec) + self.add_points(vec, colors=[colors[k]], alpha=alpha) def add_vectors(self, vectors, colors=None, alpha=1.0): """Add a list of vectors to Bloch sphere. From 3248bcb0930cff7080d4be7348fa3a9515cd5cb5 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 25 Jun 2024 09:42:03 -0400 Subject: [PATCH 249/305] Add scipy 1.14 support patch --- qutip/core/data/csr.pyx | 13 ++++++++++--- qutip/core/data/dia.pyx | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/qutip/core/data/csr.pyx b/qutip/core/data/csr.pyx index aac63a8316..5e017f8b5f 100644 --- a/qutip/core/data/csr.pyx +++ b/qutip/core/data/csr.pyx @@ -17,11 +17,18 @@ import numpy as np cimport numpy as cnp import scipy.sparse from scipy.sparse import csr_matrix as scipy_csr_matrix -try: - from scipy.sparse.data import _data_matrix as scipy_data_matrix -except ImportError: +from functools import partial +from packaging.version import parse as parse_version +if parse_version(scipy.version.version) >= parse_version("1.14.0"): + from scipy.sparse._data import _data_matrix as scipy_data_matrix + # From scipy 1.14.0, a check that the input is not scalar was added for + # sparse arrays. + scipy_data_matrix = partial(scipy_data_matrix, arg1=(0,)) +elif parse_version(scipy.version.version) >= parse_version("1.8.0"): # The file data was renamed to _data from scipy 1.8.0 from scipy.sparse._data import _data_matrix as scipy_data_matrix +else: + from scipy.sparse.data import _data_matrix as scipy_data_matrix from scipy.linalg cimport cython_blas as blas from qutip.core.data cimport base, Dense, Dia diff --git a/qutip/core/data/dia.pyx b/qutip/core/data/dia.pyx index 6dd8bd62e6..e7b2f0c6d6 100644 --- a/qutip/core/data/dia.pyx +++ b/qutip/core/data/dia.pyx @@ -17,11 +17,18 @@ import numpy as np cimport numpy as cnp import scipy.sparse from scipy.sparse import dia_matrix as scipy_dia_matrix -try: - from scipy.sparse.data import _data_matrix as scipy_data_matrix -except ImportError: +from packaging.version import parse as parse_version +from functools import partial +if parse_version(scipy.version.version) >= parse_version("1.14.0"): + from scipy.sparse._data import _data_matrix as scipy_data_matrix + # From scipy 1.14.0, a check that the input is not scalar was added for + # sparse arrays. + scipy_data_matrix = partial(scipy_data_matrix, arg1=(0,)) +elif parse_version(scipy.version.version) >= parse_version("1.8.0"): # The file data was renamed to _data from scipy 1.8.0 from scipy.sparse._data import _data_matrix as scipy_data_matrix +else: + from scipy.sparse.data import _data_matrix as scipy_data_matrix from scipy.linalg cimport cython_blas as blas from qutip.core.data cimport base, Dense, CSR From bf42a90d46aab6da1ca7f35a0db8c0038aec63da Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 25 Jun 2024 14:15:15 -0400 Subject: [PATCH 250/305] Apply comment --- qutip/solver/multitrajresult.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/solver/multitrajresult.py b/qutip/solver/multitrajresult.py index 52a12b0e8a..96ea4e8fdd 100644 --- a/qutip/solver/multitrajresult.py +++ b/qutip/solver/multitrajresult.py @@ -342,7 +342,7 @@ def _target_tolerance_end(self): # avg2 = # std * **2 = ( - **2) * **2 # = avg2 * - avg**2 - # and "<1>" is one minus the sum of all absolute weights + # and "" is one minus the sum of all absolute weights one = one - self._total_abs_weight std = avg2 * one - abs(avg)**2 @@ -830,8 +830,8 @@ def __init__(self, example_trajectory, store_states, store_final_state): else: self.sum_states = None - if (fstate := example_trajectory.final_state) and store_final_state: - self.sum_final_state = qzero_like(_to_dm(fstate)) + if example_trajectory.final_state and store_final_state: + self._initialize_sum_finalstate(example_trajectory) else: self.sum_final_state = None From 145f570b460ec6b0e50920de1daf41b43df0f881 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 25 Jun 2024 17:18:57 -0400 Subject: [PATCH 251/305] Update changlog --- doc/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.rst b/doc/changelog.rst index 1a2f2b1337..0d4afdb04a 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -29,6 +29,7 @@ Miscellaneous ------------- - Add support for numpy 2 (#2421, #2457) +- Add support for scipy 1.14 (#2469) QuTiP 5.0.2 (2024-05-16) From d31998c1b4477c15dc5df65faf42b748040c62ac Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 26 Jun 2024 13:57:43 -0400 Subject: [PATCH 252/305] Use Literal --- qutip/core/states.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/states.py b/qutip/core/states.py index f58d6b7398..99b305b5e3 100644 --- a/qutip/core/states.py +++ b/qutip/core/states.py @@ -455,7 +455,7 @@ def fock( def thermal_dm( N: int, n: float, - method: str = 'operator', + method: Literal['operator', 'analytic'] = 'operator', *, dtype: LayerType = None, ) -> Qobj: From f22acb1233b376d1f199647f9a21e0ab0a01ad2e Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 26 Jun 2024 14:12:16 -0400 Subject: [PATCH 253/305] towncrier --- doc/changes/2473.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2473.misc diff --git a/doc/changes/2473.misc b/doc/changes/2473.misc new file mode 100644 index 0000000000..103b22aeb3 --- /dev/null +++ b/doc/changes/2473.misc @@ -0,0 +1 @@ +Add type hints for Qobj creation functions. From 80ddcdbab3b175b6d46321b033649bc7e0b9323a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 26 Jun 2024 15:07:17 -0400 Subject: [PATCH 254/305] Allow merging stochastic results --- qutip/solver/stochastic.py | 48 +++++++++++++++++++++---- qutip/tests/solver/test_stochastic.py | 52 +++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 6 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index a139716c9d..f3b94ecb6f 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -113,8 +113,9 @@ def measurement(self): class StochasticResult(MultiTrajResult): - def _post_init(self): + def _post_init(self, heterodyne=False): super()._post_init() + self.heterodyne = heterodyne store_measurement = self.options["store_measurement"] keep_runs = self.options["keep_runs_results"] @@ -192,11 +193,30 @@ def wiener_process(self): return self._trajectories_attr("wiener_process") def merge(self, other, p=None): - raise NotImplementedError("Merging results of the stochastic solvers " - "is currently not supported. Please raise " - "an issue on GitHub if you would like to " - "see this feature.") + if not isinstance(other, StochasticResult): + return NotImplemented + if self.heterodyne != other.heterodyne: + raise ValueError("Can't merge heterodyne and homodyne results") + if p is not None: + raise ValueError( + "Stochastic solvers does not support custom weights" + ) + new = super().merge(other, p) + + if ( + self.options["store_measurement"] + and other.options["store_measurement"] + and not new.trajectories + ): + new._measurement = np.concatenate( + (self.measurement, other.measurement), axis=0 + ) + new._wiener_process = np.concatenate( + (self.wiener_process, other.wiener_process), axis=0 + ) + new._dW = np.concatenate((self.dW, other.dW), axis=0) + return new class _StochasticRHS(_MultiTrajRHS): """ @@ -534,7 +554,6 @@ class StochasticSolver(MultiTrajSolver): """ name = "StochasticSolver" - _resultclass = StochasticResult _avail_integrators = {} _open = None @@ -553,6 +572,15 @@ class StochasticSolver(MultiTrajSolver): "store_measurement": "", } + def _resultclass(self, e_ops, options, solver, stats): + return StochasticResult( + e_ops, + options, + solver=solver, + stats=stats, + heterodyne=self.heterodyne, + ) + def _trajectory_resultclass(self, e_ops, options): return StochasticTrajResult( e_ops, @@ -562,6 +590,14 @@ def _trajectory_resultclass(self, e_ops, options): heterodyne=self.heterodyne, ) + def _initialize_stats(self): + stats = super()._initialize_stats() + if self._open: + stats["solver"] = "Stochastic Master Equation Evolution" + else: + stats["solver"] = "Stochastic Schrodinger Equation Evolution" + return stats + def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): self._heterodyne = heterodyne if self.name == "ssesolve" and c_ops: diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index a5a28c673d..ff4dfecd0a 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -510,3 +510,55 @@ def test_run_from_experiment_open(method, heterodyne): np.testing.assert_allclose( res_measure.expect, res_forward.expect, atol=1e-10 ) + + +@pytest.mark.parametrize("store_measurement", [True, False]) +@pytest.mark.parametrize("keep_runs_results", [True, False]) +def test_merge_results(store_measurement, keep_runs_results): + # Running mcsolve with mixed ICs should be the same as running mcsolve + # multiple times and merging the results afterwards + initial_state1 = basis([2, 2], [1, 0]) + initial_state2 = basis([2, 2], [1, 1]) + H = qeye([2, 2]) + L = destroy(2) & qeye(2) + tlist = np.linspace(0, 1, 11) + e_ops = [num(2) & qeye(2), qeye(2) & num(2)] + + options = { + "store_measurement": True, + "keep_runs_results": True, + "store_states": True, + } + solver = SMESolver(H, [L], True, options=options) + result1 = solver.run(initial_state1, tlist, 5, e_ops=e_ops) + + options = { + "store_measurement": store_measurement, + "keep_runs_results": keep_runs_results, + "store_states": True, + } + solver = SMESolver(H, [L], True, options=options) + result2 = solver.run(initial_state2, tlist, 10, e_ops=e_ops) + + result_merged = result1 + result2 + assert len(result_merged.seeds) == 15 + if store_measurement: + assert ( + result_merged.average_states[0] == + (initial_state1.proj() + 2 * initial_state2.proj()).unit() + ) + np.testing.assert_allclose(result_merged.average_expect[0][0], 1) + np.testing.assert_allclose(result_merged.average_expect[1], 2/3) + + if store_measurement: + assert len(result_merged.measurement) == 15 + assert len(result_merged.dW) == 15 + assert all( + dw.shape == result_merged.dW[0].shape + for dw in result_merged.dW + ) + assert len(result_merged.wiener_process) == 15 + assert all( + w.shape == result_merged.wiener_process[0].shape + for w in result_merged.wiener_process + ) From 60cfc340c0b9452c2a40549d1c401e88e279cad2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 26 Jun 2024 15:16:38 -0400 Subject: [PATCH 255/305] Add towncrier --- doc/changes/2474.feature | 1 + qutip/solver/stochastic.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 doc/changes/2474.feature diff --git a/doc/changes/2474.feature b/doc/changes/2474.feature new file mode 100644 index 0000000000..a082c289e3 --- /dev/null +++ b/doc/changes/2474.feature @@ -0,0 +1 @@ +Allow merging results from stochastic solvers. diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index f3b94ecb6f..6566796bc7 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -218,6 +218,7 @@ def merge(self, other, p=None): return new + class _StochasticRHS(_MultiTrajRHS): """ In between object to store the stochastic system. From 5c758f5ee52d73fc100c9aa70940516727ea9525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Wed, 26 Jun 2024 16:18:47 -0400 Subject: [PATCH 256/305] Apply suggestions from code review Co-authored-by: Simon Cross --- qutip/core/gates.py | 2 +- qutip/typing.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index 3be5c53172..d58b583d8d 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -169,7 +169,7 @@ def ct_gate(*, dtype: LayerType = None) -> Qobj: ) -def rx(phi, *, dtype: LayerType = None) -> Qobj: +def rx(phi: float, *, dtype: LayerType = None) -> Qobj: """Single-qubit rotation for operator sigmax with angle phi. Parameters diff --git a/qutip/typing.py b/qutip/typing.py index 50f7f1fafa..cbc4aa066c 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -41,8 +41,6 @@ def __call__(self, t: Real, **kwargs) -> Number: DimensionLike = Union[ - list[list[int], list[int]], - list[list[list[int]], list[list[int]]], - list["Space", "Space"], + list[SpaceLike, SpaceLike], "Dimensions", ] From 12c3556bd0b5ef4939e74648a01ecc67e90c5447 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 26 Jun 2024 16:20:14 -0400 Subject: [PATCH 257/305] Add types to rotation angles --- qutip/core/gates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/core/gates.py b/qutip/core/gates.py index d58b583d8d..05e15bff3e 100644 --- a/qutip/core/gates.py +++ b/qutip/core/gates.py @@ -198,7 +198,7 @@ def rx(phi: float, *, dtype: LayerType = None) -> Qobj: ).to(dtype) -def ry(phi, *, dtype: LayerType = None) -> Qobj: +def ry(phi: float, *, dtype: LayerType = None) -> Qobj: """Single-qubit rotation for operator sigmay with angle phi. Parameters @@ -227,7 +227,7 @@ def ry(phi, *, dtype: LayerType = None) -> Qobj: ).to(dtype) -def rz(phi, *, dtype: LayerType = None) -> Qobj: +def rz(phi: float, *, dtype: LayerType = None) -> Qobj: """Single-qubit rotation for operator sigmaz with angle phi. Parameters From e32afe66e7010ae2205b74bad48f14fbacd702ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Thu, 27 Jun 2024 10:07:02 -0400 Subject: [PATCH 258/305] Update qutip/tests/solver/test_stochastic.py Co-authored-by: Paul --- qutip/tests/solver/test_stochastic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index ff4dfecd0a..cf63bef666 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -515,7 +515,7 @@ def test_run_from_experiment_open(method, heterodyne): @pytest.mark.parametrize("store_measurement", [True, False]) @pytest.mark.parametrize("keep_runs_results", [True, False]) def test_merge_results(store_measurement, keep_runs_results): - # Running mcsolve with mixed ICs should be the same as running mcsolve + # Running smesolve with mixed ICs should be the same as running smesolve # multiple times and merging the results afterwards initial_state1 = basis([2, 2], [1, 0]) initial_state2 = basis([2, 2], [1, 1]) From 1756d946b0a3b40090a116412775d1f9d8ea58d5 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 27 Jun 2024 10:47:19 -0400 Subject: [PATCH 259/305] Ensure ssesolve and smesolve result can't be merged --- qutip/solver/stochastic.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 6566796bc7..7606259c86 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -195,6 +195,8 @@ def wiener_process(self): def merge(self, other, p=None): if not isinstance(other, StochasticResult): return NotImplemented + if self.stats["solver"] != other.stats["solver"]: + raise ValueError("Can't merge smesolve and ssesolve results") if self.heterodyne != other.heterodyne: raise ValueError("Can't merge heterodyne and homodyne results") if p is not None: From 53bc585be287218b59da455772256ae9ebd9c9b2 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 28 Jun 2024 06:34:10 -0400 Subject: [PATCH 260/305] Add run_from_exp example --- doc/guide/dynamics/dynamics-stochastic.rst | 41 +++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index 8e60bc2c44..d0639bdd50 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -178,14 +178,12 @@ in ``result.measurements``. ax.set_xlabel('Time') ax.legend() -.. - TODO merge qutip-tutorials#61 - For other examples on :func:`qutip.solver.stochastic.smesolve`, see the - `following notebook <...>`_, as well as these notebooks available at - `QuTiP Tutorials page `_: - `heterodyne detection <...>`_, - `inefficient detection <...>`_, and - `feedback control `_. +For other examples on :func:`qutip.solver.stochastic.smesolve`, see the +notebooks available at `QuTiP Tutorials page `_: + +`heterodyne detection `_, +`inefficient detection `_, and +`feedback control `_. The stochastic solvers share many features with :func:`.mcsolve`, such as @@ -193,6 +191,33 @@ end conditions, seed control and running in parallel. See the sections :ref:`monte-ntraj`, :ref:`monte-seeds` and :ref:`monte-parallel` for details. +Run with known noise +==================== + +In situations where instead of running multiple trajectories, we want to reproduce a single trajectory from known measurements or noise. +In these cases, we can use :method:`~qutip.solver.stochastic.SMESolver.run_from_experiment`. +We can rerun the first trajectory of the previous simulation with: + +.. code-block:: + + # Use the class + solver = SMESolver( + H, sc_ops=[np.sqrt(KAPPA) * a], + options={"dt": 0.00125, "store_measurement": True,} + ) + + recreated_solution = solver.run_from_experiment( + rho_0, + e_ops=[H], + noise=stoc_solution.measurements[0] + measurement=True, + ) + +This will recompute the wiener increment and expectation values for that trajectory. + +When using this mode of evolution, the measurement use the state at the beginning of the time step, instead of the end as per normal evolutions' default. + + .. plot:: :context: reset :include-source: false From 90e270f6358a500ddc0678e31b3471dabdde769b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 28 Jun 2024 12:40:10 -0400 Subject: [PATCH 261/305] Finish explanation --- doc/guide/dynamics/dynamics-stochastic.rst | 61 +++++++++++++--------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index d0639bdd50..fd1e1e3ed4 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -130,16 +130,21 @@ Example Below, we solve the dynamics for an optical cavity at 0K whose output is monitored using homodyne detection. The cavity decay rate is given by :math:`\kappa` and the :math:`\Delta` is the cavity detuning with respect to the driving field. -The measurement operators can be passed using the option ``m_ops``. The homodyne -current :math:`J_x` is calculated using +The homodyne current :math:`J_x` is calculated using .. math:: :label: measurement_result J_x = \langle x \rangle + dW / dt, -where :math:`x` is the operator passed using ``m_ops``. The results are available -in ``result.measurements``. +where :math:`x` is the operator build from the ``sc_ops`` as + +.. math:: + + x_n = S_n + S_n^\dagger + + +The results are available in ``result.measurements``. .. plot:: :context: reset @@ -178,45 +183,49 @@ in ``result.measurements``. ax.set_xlabel('Time') ax.legend() -For other examples on :func:`qutip.solver.stochastic.smesolve`, see the -notebooks available at `QuTiP Tutorials page `_: - -`heterodyne detection `_, -`inefficient detection `_, and -`feedback control `_. - - -The stochastic solvers share many features with :func:`.mcsolve`, such as -end conditions, seed control and running in parallel. See the sections -:ref:`monte-ntraj`, :ref:`monte-seeds` and :ref:`monte-parallel` for details. +Run from known measurements +=========================== -Run with known noise -==================== - -In situations where instead of running multiple trajectories, we want to reproduce a single trajectory from known measurements or noise. +In situations where instead of running multiple trajectories, we want to reproduce a single trajectory from known noise or measurements obtained in lab. In these cases, we can use :method:`~qutip.solver.stochastic.SMESolver.run_from_experiment`. -We can rerun the first trajectory of the previous simulation with: + +Let use the measurement output ``J_x`` of the first trajectory of the previous simulation as the input to recompute a trajectory: .. code-block:: - # Use the class + # Only available from the class interface. solver = SMESolver( H, sc_ops=[np.sqrt(KAPPA) * a], options={"dt": 0.00125, "store_measurement": True,} ) recreated_solution = solver.run_from_experiment( - rho_0, + rho_0, tlist, + noise=stoc_solution.measurements[0], e_ops=[H], - noise=stoc_solution.measurements[0] - measurement=True, + measurement=True, # The ``noise`` input is a measurement. ) -This will recompute the wiener increment and expectation values for that trajectory. +This will recompute the states, expectation values and wiener increments for that trajectory. + +.. note:: + + The measurements in the result is computed from the state at the end of the time step. + However, when using ``run_from_experiment`` with measurement input, the state at the start of the time step is used. -When using this mode of evolution, the measurement use the state at the beginning of the time step, instead of the end as per normal evolutions' default. +For other examples on :func:`qutip.solver.stochastic.smesolve`, see the +notebooks available at `QuTiP Tutorials page `_: + +`heterodyne detection `_, +`inefficient detection `_, and +`feedback control `_. + + +The stochastic solvers share many features with :func:`.mcsolve`, such as +end conditions, seed control and running in parallel. See the sections +:ref:`monte-ntraj`, :ref:`monte-seeds` and :ref:`monte-parallel` for details. .. plot:: :context: reset From 9cf5591496cd5778a2d167f3c484455261c2d90c Mon Sep 17 00:00:00 2001 From: Andrey Nikitin <143126464+pyukey@users.noreply.github.com> Date: Fri, 28 Jun 2024 19:38:50 -0400 Subject: [PATCH 262/305] Shortened 2466.bugfix --- doc/changes/2466.bugfix | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/doc/changes/2466.bugfix b/doc/changes/2466.bugfix index 2bba02592e..7f04e25dbf 100644 --- a/doc/changes/2466.bugfix +++ b/doc/changes/2466.bugfix @@ -1,4 +1 @@ -Fixed issue #1919 -Dicke density state matrix should only have positive eigenvalues, but rounding errors causes eigenvalues of 0 to instead be represented as a very small negative number. -This causes dicke_trace_function to return -Inf, since scipy.special.entr returns -Inf for any values < 0. -This issue was fixed by simply taking the absolute value of the eigenvalues in dicke_trace_function. \ No newline at end of file +Fixed rounding error in dicke_trace_function that resulted in negative eigenvalues. From f080985fe9a953e5bc33169a1c7371aea0745994 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 2 Jul 2024 08:10:55 -0400 Subject: [PATCH 263/305] Improve wording --- doc/guide/dynamics/dynamics-stochastic.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index fd1e1e3ed4..6007e5c226 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -194,17 +194,19 @@ Let use the measurement output ``J_x`` of the first trajectory of the previous s .. code-block:: - # Only available from the class interface. + # Create a stochastic solver instance with the some Hamiltonian as the + # previous evolution. solver = SMESolver( H, sc_ops=[np.sqrt(KAPPA) * a], options={"dt": 0.00125, "store_measurement": True,} ) + # Run the evolution, noise recreated_solution = solver.run_from_experiment( - rho_0, tlist, - noise=stoc_solution.measurements[0], + rho_0, tlist, stoc_solution.measurements[0], e_ops=[H], - measurement=True, # The ``noise`` input is a measurement. + # The third parameter is a measurement, not Gaussian noise + measurement=True, ) This will recompute the states, expectation values and wiener increments for that trajectory. From c05b4deef5974ad7da3421ac6b0166f248b03b0f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 2 Jul 2024 09:40:33 -0400 Subject: [PATCH 264/305] Fix keyword --- doc/guide/dynamics/dynamics-stochastic.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index 6007e5c226..645e015421 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -188,7 +188,7 @@ Run from known measurements =========================== In situations where instead of running multiple trajectories, we want to reproduce a single trajectory from known noise or measurements obtained in lab. -In these cases, we can use :method:`~qutip.solver.stochastic.SMESolver.run_from_experiment`. +In these cases, we can use :meth:`~qutip.solver.stochastic.SMESolver.run_from_experiment`. Let use the measurement output ``J_x`` of the first trajectory of the previous simulation as the input to recompute a trajectory: From 52f8ea2017f6245f08f5d1f57ad3ca8f81436534 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 4 Jul 2024 10:27:59 -0400 Subject: [PATCH 265/305] hints in tensor and super --- qutip/core/operators.py | 20 +++++++++- qutip/core/properties.py | 14 +++---- qutip/core/qobj.py | 28 +++++++------- qutip/core/superoperator.py | 77 ++++++++++++++++++++++++++++++++----- qutip/core/tensor.py | 8 +++- 5 files changed, 112 insertions(+), 35 deletions(-) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index e09613f68c..ad3ed58972 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -13,7 +13,7 @@ ] import numpy as np -from typing import Literal +from typing import Literal, overload from . import data as _data from .qobj import Qobj from .dimensions import Space @@ -85,9 +85,25 @@ def qdiags( ) +@overload def jmat( j: float, - which: Literal["x", "y", "z", "+", "-"] = None, + which: NoneType, + *, + dtype: LayerType = None +) -> tuple[Qobj]: ... + +@overload +def jmat( + j: float, + which: Literal["x", "y", "z", "+", "-"], + *, + dtype: LayerType = None +) -> Qobj: ... + +def jmat( + j: float, + which: Literal["x", "y", "z", "+", "-", None] = None, *, dtype: LayerType = None ) -> Qobj | tuple[Qobj]: diff --git a/qutip/core/properties.py b/qutip/core/properties.py index 5bfa417bb0..17016050d3 100644 --- a/qutip/core/properties.py +++ b/qutip/core/properties.py @@ -5,31 +5,31 @@ ] -def isbra(x: Qobj | QobjEvo): +def isbra(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['bra', 'scalar'] -def isket(x: Qobj | QobjEvo): +def isket(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['ket', 'scalar'] -def isoper(x: Qobj | QobjEvo): +def isoper(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['oper', 'scalar'] -def isoperbra(x: Qobj | QobjEvo): +def isoperbra(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-bra'] -def isoperket(x: Qobj | QobjEvo): +def isoperket(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['operator-ket'] -def issuper(x: Qobj | QobjEvo): +def issuper(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['super'] def isherm(x: Qobj): - if not isinstance(x, Qobj): + if not isinstance(x, Qobj) -> bool: raise TypeError(f"Invalid input type, got {type(x)}, exected Qobj") return x.isherm diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index d8c41588ec..e9d71b2922 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -113,12 +113,10 @@ class Qobj: Parameters ---------- - inpt: array_like, data object or :obj:`.Qobj` + arg: array_like, data object or :obj:`.Qobj` Data for vector/matrix representation of the quantum object. dims: list Dimensions of object used for tensor products. - shape: list - Shape of underlying data structure (matrix shape). copy: bool Flag specifying whether Qobj should get a copy of the input data, or use the original. @@ -373,7 +371,7 @@ def to(self, data_type: LayerType, copy: bool=False) -> Qobj: ) @_require_equal_type - def __add__(self, other: Qobj | numbers.Number) -> Qobj: + def __add__(self, other: Qobj | complex) -> Qobj: if other == 0: return self.copy() return Qobj(_data.add(self._data, other._data), @@ -381,11 +379,11 @@ def __add__(self, other: Qobj | numbers.Number) -> Qobj: isherm=(self._isherm and other._isherm) or None, copy=False) - def __radd__(self, other: Qobj | numbers.Number) -> Qobj: + def __radd__(self, other: Qobj | complex) -> Qobj: return self.__add__(other) @_require_equal_type - def __sub__(self, other: Qobj | numbers.Number) -> Qobj: + def __sub__(self, other: Qobj | complex) -> Qobj: if other == 0: return self.copy() return Qobj(_data.sub(self._data, other._data), @@ -393,10 +391,10 @@ def __sub__(self, other: Qobj | numbers.Number) -> Qobj: isherm=(self._isherm and other._isherm) or None, copy=False) - def __rsub__(self, other: Qobj | numbers.Number) -> Qobj: + def __rsub__(self, other: Qobj | complex) -> Qobj: return self.__neg__().__add__(other) - def __mul__(self, other: numbers.Number) -> Qobj: + def __mul__(self, other: complex) -> Qobj: """ If other is a Qobj, we dispatch to __matmul__. If not, we check that other is a valid complex scalar, i.e., we can do @@ -430,7 +428,7 @@ def __mul__(self, other: numbers.Number) -> Qobj: isunitary=isunitary, copy=False) - def __rmul__(self, other: numbers.Number) -> Qobj: + def __rmul__(self, other: complex) -> Qobj: # Shouldn't be here unless `other.__mul__` has already been tried, so # we _shouldn't_ check that `other` is `Qobj`. return self.__mul__(other) @@ -452,7 +450,7 @@ def __matmul__(self, other: Qobj) -> Qobj: copy=False ) - def __truediv__(self, other: numbers.Number) -> Qobj: + def __truediv__(self, other: complex) -> Qobj: return self.__mul__(1 / other) def __neg__(self) -> Qobj: @@ -648,7 +646,7 @@ def norm( self, norm: Literal["l2", "max", "fro", "tr", "one"] = None, kwargs: dict[str, Any] = None - ) -> numbers.Number: + ) -> float: """ Norm of a quantum object. @@ -708,7 +706,7 @@ def proj(self) -> Qobj: isherm=True, copy=False) - def tr(self) -> numbers.Number: + def tr(self) -> complex: """Trace of a quantum object. Returns @@ -724,7 +722,7 @@ def tr(self) -> numbers.Number: out = out.real return out - def purity(self) -> numbers.Number: + def purity(self) -> complex: """Calculate purity of a quantum object. Returns @@ -1390,7 +1388,7 @@ def matrix_element(self, bra: Qobj, ket: Qobj) -> Qobj: right = right.adjoint() return _data.inner_op(left, op, right, bra.isket) - def overlap(self, other: Qobj) -> numbers.Number: + def overlap(self, other: Qobj) -> complex: """ Overlap between two state vectors or two operators. @@ -1624,7 +1622,7 @@ def groundstate( warnings.warn("Ground state may be degenerate.", UserWarning) return evals[0], evecs[0] - def dnorm(self, B: Qobj = None) -> numbers.Number: + def dnorm(self, B: Qobj = None) -> float: """Calculates the diamond norm, or the diamond distance to another operator. diff --git a/qutip/core/superoperator.py b/qutip/core/superoperator.py index 68488a8cd9..c493405ead 100644 --- a/qutip/core/superoperator.py +++ b/qutip/core/superoperator.py @@ -5,7 +5,7 @@ ] import functools - +from typing import TypeVar, overload import numpy as np from .qobj import Qobj @@ -30,7 +30,28 @@ def out(qobj): return out -def liouvillian(H=None, c_ops=None, data_only=False, chi=None): +@overload +def liouvillian( + H: Qobj, + c_ops: list[Qobj], + data_only: bool, + chi: list[float] +) -> Qobj: ... + +@overload +def liouvillian( + H: Qobj | "QobjEvo", + c_ops: list[Qobj | "QobjEvo"], + data_only: bool, + chi: list[float] +) -> "QobjEvo": ... + +def liouvillian( + H: Qobj | "QobjEvo" = None, + c_ops: list[Qobj | "QobjEvo"] = None, + data_only: bool = False, + chi: list[float] = None, +) -> Qobj | "QobjEvo": """Assembles the Liouvillian superoperator from a Hamiltonian and a ``list`` of collapse operators. @@ -118,7 +139,28 @@ def liouvillian(H=None, c_ops=None, data_only=False, chi=None): copy=False) -def lindblad_dissipator(a, b=None, data_only=False, chi=None): +@overload +def lindblad_dissipator( + a: Qobj, + b: Qobj, + data_only: bool, + chi: list[float] +) -> Qobj: ... + +@overload +def lindblad_dissipator( + a: Qobj | "QobjEvo", + b: Qobj | "QobjEvo", + data_only: bool, + chi: list[float] +) -> "QobjEvo": ... + +def lindblad_dissipator( + a: Qobj | "QobjEvo", + b: Qobj | "QobjEvo" = None, + data_only: bool = False, + chi: list[float] = None, +) -> Qobj | "QobjEvo": """ Lindblad dissipator (generalized) for a single pair of collapse operators (a, b), or for a single collapse operator (a) when b is not specified: @@ -180,7 +222,7 @@ def lindblad_dissipator(a, b=None, data_only=False, chi=None): @_map_over_compound_operators -def operator_to_vector(op): +def operator_to_vector(op: Qobj) -> Qobj: """ Create a vector representation given a quantum operator in matrix form. The passed object should have a ``Qobj.type`` of 'oper' or 'super'; this @@ -208,7 +250,7 @@ def operator_to_vector(op): @_map_over_compound_operators -def vector_to_operator(op): +def vector_to_operator(op: Qobj) -> Qobj: """ Create a matrix representation given a quantum operator in vector form. The passed object should have a ``Qobj.type`` of 'operator-ket'; this @@ -236,7 +278,10 @@ def vector_to_operator(op): copy=False) -def stack_columns(matrix): +QobjOrArray = TypeVar("QobjOrArray", Qobj, np.ndarray) + + +def stack_columns(matrix: QobjOrArray) -> QobjOrArray: """ Stack the columns in a data-layer type, useful for converting an operator into a superoperator representation. @@ -250,7 +295,10 @@ def stack_columns(matrix): return _data.column_stack(matrix) -def unstack_columns(vector, shape=None): +def unstack_columns( + vector: QobjOrArray, + shape: typle[int, int] = None, +) -> QobjOrArray: """ Unstack the columns in a data-layer type back into a 2D shape, useful for converting an operator in vector form back into a regular operator. If @@ -295,8 +343,11 @@ def stacked_index(size, row, col): return row + size*col +AnyQobj = TypeVar("AnyQobj", Qobj, "QobjEvo") + + @_map_over_compound_operators -def spost(A): +def spost(A: AnyQobj) -> AnyQobj: """ Superoperator formed from post-multiplication by operator A @@ -321,7 +372,7 @@ def spost(A): @_map_over_compound_operators -def spre(A): +def spre(A: AnyQobj) -> AnyQobj: """Superoperator formed from pre-multiplication by operator A. Parameters @@ -352,6 +403,12 @@ def _drop_projected_dims(dims): return [d for d in dims if d != 1] +@overload +sprepost(A: Qobj, B: Qobj) -> Qobj: ... + +@overload +sprepost(A: Qobj | "QobjEvo", B: Qobj | "QobjEvo") -> "QobjEvo": ... + def sprepost(A, B): """ Superoperator formed from pre-multiplication by A and post-multiplication @@ -468,7 +525,7 @@ def _to_tensor_of_super(q_oper): return q_oper.permute(perm_idxs) -def reshuffle(q_oper): +def reshuffle(q_oper: Qobj) -> Qobj: """ Column-reshuffles a super operator or a operator-ket Qobj. """ diff --git a/qutip/core/tensor.py b/qutip/core/tensor.py index b17e06d01c..ec8ed0af37 100644 --- a/qutip/core/tensor.py +++ b/qutip/core/tensor.py @@ -29,7 +29,13 @@ def __call__(self, op): return tensor(op, self.right) -def tensor(*args): +@overload +def tensor(*args: Qobj) -> Qobj: ... + +@overload +def tensor(*args: Qobj | "QobjEvo") -> "QobjEvo": ... + +def tensor(*args: Qobj | "QobjEvo") -> Qobj | "QobjEvo": """Calculates the tensor product of input operators. Parameters From ec017194e386aae0517db5389670ba70f11ca19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Thu, 4 Jul 2024 10:34:27 -0400 Subject: [PATCH 266/305] Apply suggestions from code review Co-authored-by: Paul --- doc/guide/dynamics/dynamics-stochastic.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index 645e015421..1bbda8e7e8 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -144,7 +144,7 @@ where :math:`x` is the operator build from the ``sc_ops`` as x_n = S_n + S_n^\dagger -The results are available in ``result.measurements``. +The results are available in ``result.measurement``. .. plot:: :context: reset @@ -205,7 +205,7 @@ Let use the measurement output ``J_x`` of the first trajectory of the previous s recreated_solution = solver.run_from_experiment( rho_0, tlist, stoc_solution.measurements[0], e_ops=[H], - # The third parameter is a measurement, not Gaussian noise + # The third parameter is the measurement, not the Wiener increment measurement=True, ) @@ -213,16 +213,17 @@ This will recompute the states, expectation values and wiener increments for tha .. note:: - The measurements in the result is computed from the state at the end of the time step. + The measurement in the result is by default computed from the state at the end of the time step. However, when using ``run_from_experiment`` with measurement input, the state at the start of the time step is used. + To obtain the measurement at the start of the time step in the output of ``smesolve``, one may use the option ``{'store_measurement': 'start'}``. For other examples on :func:`qutip.solver.stochastic.smesolve`, see the -notebooks available at `QuTiP Tutorials page `_: +notebooks available on the `QuTiP Tutorials page `_: -`heterodyne detection `_, -`inefficient detection `_, and -`feedback control `_. +* `Heterodyne detection `_ +* `Inefficient detection `_ +* `Feedback control `_ The stochastic solvers share many features with :func:`.mcsolve`, such as From 78e3b336517fa69edb5cd20182597f5c1cf4b14d Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 4 Jul 2024 10:39:16 -0400 Subject: [PATCH 267/305] Remove outside notebook link --- doc/guide/dynamics/dynamics-stochastic.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/guide/dynamics/dynamics-stochastic.rst b/doc/guide/dynamics/dynamics-stochastic.rst index 1bbda8e7e8..025720cc0e 100644 --- a/doc/guide/dynamics/dynamics-stochastic.rst +++ b/doc/guide/dynamics/dynamics-stochastic.rst @@ -223,7 +223,10 @@ notebooks available on the `QuTiP Tutorials page `_ * `Inefficient detection `_ -* `Feedback control `_ + +.. + TODO: Add back when the notebook is migrated + * `Feedback control `_ The stochastic solvers share many features with :func:`.mcsolve`, such as From d06f5555d057111b38f15bf99dab101ef99e3177 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 4 Jul 2024 17:05:13 -0400 Subject: [PATCH 268/305] Add types in core --- qutip/core/dimensions.py | 140 +++++++++++++++++++----------------- qutip/core/operators.py | 2 +- qutip/core/properties.py | 4 +- qutip/core/qobj.py | 53 ++++++-------- qutip/core/superoperator.py | 33 ++++----- qutip/core/tensor.py | 52 +++++++++++--- 6 files changed, 158 insertions(+), 126 deletions(-) diff --git a/qutip/core/dimensions.py b/qutip/core/dimensions.py index f19e3b2dee..e92e8d2a45 100644 --- a/qutip/core/dimensions.py +++ b/qutip/core/dimensions.py @@ -8,7 +8,9 @@ import numbers from operator import getitem from functools import partial +from typing import Literal from qutip.settings import settings +from qutip.typing import SpaceLike, DimensionLike __all__ = ["to_tensor_rep", "from_tensor_rep", "Space", "Dimensions"] @@ -351,7 +353,7 @@ def _frozen(*args, **kwargs): class MetaSpace(type): - def __call__(cls, *args, rep=None): + def __call__(cls, *args: SpaceLike, rep: str = None) -> Space: """ Select which subclass is instantiated. """ @@ -399,7 +401,11 @@ def __call__(cls, *args, rep=None): cls._stored_dims[args] = instance return cls._stored_dims[args] - def from_list(cls, list_dims, rep=None): + def from_list( + cls, + list_dims: list[int] | list[list[int]], + rep: str = None + ) -> Space: if len(list_dims) == 0: raise ValueError("Empty list can't be used as dims.") elif ( @@ -449,7 +455,7 @@ def __init__(self, dims): self._pure_dims = True self.__setitem__ = _frozen - def __eq__(self, other): + def __eq__(self, other) -> bool: return self is other or ( type(other) is type(self) and other.size == self.size @@ -458,16 +464,16 @@ def __eq__(self, other): def __hash__(self): return hash(self.size) - def __repr__(self): + def __repr__(self) -> str: return f"Space({self.size})" - def as_list(self): + def as_list(self) -> list[int]: return [self.size] - def __str__(self): + def __str__(self) -> str: return str(self.as_list()) - def dims2idx(self, dims): + def dims2idx(self, dims: list[int]) -> int: """ Transform dimensions indices to full array indices. """ @@ -479,7 +485,7 @@ def dims2idx(self, dims): raise TypeError("Dimensions must be integers") return dims[0] - def idx2dims(self, idx): + def idx2dims(self, idx: int) -> list[int]: """ Transform full array indices to dimensions indices. """ @@ -487,7 +493,7 @@ def idx2dims(self, idx): raise IndexError("Index out of range") return [idx] - def step(self): + def step(self) -> list[int]: """ Get the step in the array between for each dimensions index. @@ -496,11 +502,11 @@ def step(self): """ return [1] - def flat(self): + def flat(self) -> list[int]: """ Dimensions as a flat list. """ return [self.size] - def remove(self, idx): + def remove(self, idx: int): """ Remove a Space from a Dimensons or complex Space. @@ -508,7 +514,7 @@ def remove(self, idx): """ raise RuntimeError("Cannot delete a flat space.") - def replace(self, idx, new): + def replace(self, idx: int, new: int) -> Space: """ Reshape a Space from a Dimensons or complex Space. @@ -520,10 +526,10 @@ def replace(self, idx, new): ) return Space(new) - def replace_superrep(self, super_rep): + def replace_superrep(self, super_rep: str) -> Space: return self - def scalar_like(self): + def scalar_like(self) -> SpaceLike: return Field() @@ -537,28 +543,28 @@ def __init__(self): self._pure_dims = True self.__setitem__ = _frozen - def __eq__(self, other): + def __eq__(self, other) -> bool: return type(other) is Field def __hash__(self): return hash(0) - def __repr__(self): + def __repr__(self) -> str: return "Field()" - def as_list(self): + def as_list(self) -> list[int]: return [1] - def step(self): + def step(self) -> list[int]: return [1] - def flat(self): + def flat(self) -> list[int]: return [1] - def remove(self, idx): + def remove(self, idx: int) -> Space: return self - def replace(self, idx, new): + def replace(self, idx: int, new: int) -> Space: return Space(new) @@ -570,15 +576,15 @@ class Compound(Space): _stored_dims = {} def __init__(self, *spaces): - self.spaces = [] + spaces_ = [] if len(spaces) <= 1: raise ValueError("Compound need multiple space to join.") for space in spaces: if isinstance(space, Compound): - self.spaces += space.spaces + spaces_ += space.spaces else: - self.spaces += [space] - self.spaces = tuple(self.spaces) + spaces_ += [space] + self.spaces = tuple(spaces_) self.size = np.prod([space.size for space in self.spaces]) self.issuper = all(space.issuper for space in self.spaces) if not self.issuper and any(space.issuper for space in self.spaces): @@ -596,7 +602,7 @@ def __init__(self, *spaces): ) self.__setitem__ = _frozen - def __eq__(self, other): + def __eq__(self, other) -> bool: return self is other or ( type(other) is type(self) and self.spaces == other.spaces @@ -605,14 +611,14 @@ def __eq__(self, other): def __hash__(self): return hash(self.spaces) - def __repr__(self): + def __repr__(self) -> str: parts_rep = ", ".join(repr(space) for space in self.spaces) return f"Compound({parts_rep})" - def as_list(self): + def as_list(self) -> list[int]: return sum([space.as_list() for space in self.spaces], []) - def dims2idx(self, dims): + def dims2idx(self, dims-> list[int]) -> int: if len(dims) != len(self.spaces): raise ValueError("Length of supplied dims does not match the number of subspaces.") pos = 0 @@ -622,14 +628,14 @@ def dims2idx(self, dims): step *= space.size return pos - def idx2dims(self, idx): + def idx2dims(self, idx: int) -> list[int]: dims = [] for space in self.spaces[::-1]: idx, dim = divmod(idx, space.size) dims = space.idx2dims(dim) + dims return dims - def step(self): + def step(self) -> list[int]: steps = [] step = 1 for space in self.spaces[::-1]: @@ -637,10 +643,10 @@ def step(self): step *= space.size return steps - def flat(self): + def flat(self) -> list[int]: return sum([space.flat() for space in self.spaces], []) - def remove(self, idx): + def remove(self, idx: int) -> Space: new_spaces = [] for space in self.spaces: n_indices = len(space.flat()) @@ -651,7 +657,7 @@ def remove(self, idx): return Compound(*new_spaces) return Field() - def replace(self, idx, new): + def replace(self, idx: int, new: int) -> Space: new_spaces = [] for space in self.spaces: n_indices = len(space.flat()) @@ -662,19 +668,19 @@ def replace(self, idx, new): idx -= n_indices return Compound(*new_spaces) - def replace_superrep(self, super_rep): + def replace_superrep(self, super_rep: str) -> Space: return Compound( *[space.replace_superrep(super_rep) for space in self.spaces] ) - def scalar_like(self): - return [space.scalar_like() for space in self.spaces] + def scalar_like(self) -> Space: + return Compound([space.scalar_like() for space in self.spaces]) class SuperSpace(Space): _stored_dims = {} - def __init__(self, oper, rep='super'): + def __init__(self, oper: Dimensions, rep: str = 'super'): self.oper = oper self.superrep = rep self.size = oper.shape[0] * oper.shape[1] @@ -682,7 +688,7 @@ def __init__(self, oper, rep='super'): self._pure_dims = oper._pure_dims self.__setitem__ = _frozen - def __eq__(self, other): + def __eq__(self, other) -> bool: return ( self is other or self.oper == other @@ -696,47 +702,47 @@ def __eq__(self, other): def __hash__(self): return hash((self.oper, self.superrep)) - def __repr__(self): + def __repr__(self) -> str: return f"Super({repr(self.oper)}, rep={self.superrep})" - def as_list(self): + def as_list(self) -> list[list[int]]: return self.oper.as_list() - def dims2idx(self, dims): + def dims2idx(self, dims: list[int]) -> int: posl, posr = self.oper.dims2idx(dims) return posl + posr * self.oper.shape[0] - def idx2dims(self, idx): + def idx2dims(self, idx: int) -> list[int]: posl = idx % self.oper.shape[0] posr = idx // self.oper.shape[0] return self.oper.idx2dims(posl, posr) - def step(self): + def step(self) -> list[int]: stepl, stepr = self.oper.step() step = self.oper.shape[0] return stepl + [step * N for N in stepr] - def flat(self): + def flat(self) -> list[int]: return sum(self.oper.flat(), []) - def remove(self, idx): + def remove(self, idx: int) -> Space: new_dims = self.oper.remove(idx) if new_dims.type == 'scalar': return Field() return SuperSpace(new_dims, rep=self.superrep) - def replace(self, idx, new): + def replace(self, idx: int, new: int) -> Space: return SuperSpace(self.oper.replace(idx, new), rep=self.superrep) - def replace_superrep(self, super_rep): + def replace_superrep(self, super_rep: str) -> Space: return SuperSpace(self.oper, rep=super_rep) - def scalar_like(self): - return self.oper.scalar_like() + def scalar_like(self) -> Space: + return SuperSpace(self.oper.scalar_like(), rep=self.superrep) class MetaDims(type): - def __call__(cls, *args, rep=None): + def __call__(cls, *args: DimensionLike, rep: str = None) -> Dimensions: if len(args) == 1 and isinstance(args[0], Dimensions): return args[0] elif len(args) == 1 and len(args[0]) == 2: @@ -761,9 +767,9 @@ def __call__(cls, *args, rep=None): class Dimensions(metaclass=MetaDims): _stored_dims = {} - _type = None + _type: str = None - def __init__(self, from_, to_): + def __init__(self, from_: Space, to_: Space): self.from_ = from_ self.to_ = to_ self.shape = to_.size, from_.size @@ -801,7 +807,7 @@ def __init__(self, from_, to_): self.superrep = 'mixed' self.__setitem__ = _frozen - def __eq__(self, other): + def __eq__(self, other: Dimensions) -> bool: if isinstance(other, Dimensions): return ( self is other @@ -812,7 +818,7 @@ def __eq__(self, other): ) return NotImplemented - def __ne__(self, other): + def __ne__(self, other: Dimensions) -> bool: if isinstance(other, Dimensions): return not ( self is other @@ -823,7 +829,7 @@ def __ne__(self, other): ) return NotImplemented - def __matmul__(self, other): + def __matmul__(self, other: Dimensions) -> Dimensions: if self.from_ != other.to_: raise TypeError(f"incompatible dimensions {self} and {other}") args = other.from_, self.to_ @@ -834,19 +840,19 @@ def __matmul__(self, other): def __hash__(self): return hash((self.to_, self.from_)) - def __repr__(self): + def __repr__(self) -> str: return f"Dimensions({repr(self.from_)}, {repr(self.to_)})" - def __str__(self): + def __str__(self) -> str: return str(self.as_list()) - def as_list(self): + def as_list(self) -> list: """ Return the list representation of the Dimensions object. """ return [self.to_.as_list(), self.from_.as_list()] - def __getitem__(self, key): + def __getitem__(self, key: Literal[0, 1]) -> Space: if key == 0: return self.to_ elif key == 1: @@ -865,7 +871,7 @@ def idx2dims(self, idxl, idxr): """ return [self.to_.idx2dims(idxl), self.from_.idx2dims(idxr)] - def step(self): + def step(self) -> list[list[int]]: """ Get the step in the array between for each dimensions index. @@ -874,7 +880,7 @@ def step(self): """ return [self.to_.step(), self.from_.step()] - def flat(self): + def flat(self) -> list[list[int]]: """ Dimensions as a flat list. """ return [self.to_.flat(), self.from_.flat()] @@ -907,7 +913,7 @@ def _get_tensor_perm(self): np.argsort(stepr)[::-1] + len(stepl) ])) - def remove(self, idx): + def remove(self, idx: int | list[int]) -> Dimensions: """ Remove a Space from a Dimensons or complex Space. @@ -926,7 +932,7 @@ def remove(self, idx): self.to_.remove(idx_to), ) - def replace(self, idx, new): + def replace(self, idx: int, new: int) -> Dimensions: """ Reshape a Space from a Dimensons or complex Space. @@ -942,7 +948,7 @@ def replace(self, idx, new): return Dimensions(new_from, new_to) - def replace_superrep(self, super_rep): + def replace_superrep(self, super_rep: str) -> Dimensions: if not self.issuper and super_rep is not None: raise TypeError("Can't set a superrep of a non super object.") return Dimensions( @@ -950,5 +956,5 @@ def replace_superrep(self, super_rep): self.to_.replace_superrep(super_rep) ) - def scalar_like(self): - return [self.to_.scalar_like(), self.from_.scalar_like()] + def scalar_like(self) -> Dimensions: + return Dimensions([self.to_.scalar_like(), self.from_.scalar_like()]) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index ad3ed58972..e51bc11783 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -88,7 +88,7 @@ def qdiags( @overload def jmat( j: float, - which: NoneType, + which: Literal[None], *, dtype: LayerType = None ) -> tuple[Qobj]: ... diff --git a/qutip/core/properties.py b/qutip/core/properties.py index 17016050d3..8e9ade0270 100644 --- a/qutip/core/properties.py +++ b/qutip/core/properties.py @@ -29,7 +29,7 @@ def issuper(x: Qobj | QobjEvo) -> bool: return isinstance(x, (Qobj, QobjEvo)) and x.type in ['super'] -def isherm(x: Qobj): - if not isinstance(x, Qobj) -> bool: +def isherm(x: Qobj) -> bool: + if not isinstance(x, Qobj): raise TypeError(f"Invalid input type, got {type(x)}, exected Qobj") return x.isherm diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index e9d71b2922..84d5e5bca0 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -15,6 +15,7 @@ from ..settings import settings from . import data as _data from qutip.typing import LayerType, DimensionLike +import qutip from .dimensions import ( enumerate_flat, collapse_dims_super, flatten, unflatten, Dimensions ) @@ -316,10 +317,6 @@ def superrep(self, super_rep: str): def data(self) -> _data.Data: return self._data - @property - def dtype(self): - return type(self._data) - @data.setter def data(self, data: _data.Data): if not isinstance(data, _data.Data): @@ -329,6 +326,10 @@ def data(self, data: _data.Data): f"{self._dims.shape} vs {data.shape}") self._data = data + @property + def dtype(self): + return type(self._data) + def to(self, data_type: LayerType, copy: bool=False) -> Qobj: """ Convert the underlying data store of this `Qobj` into a different @@ -544,7 +545,7 @@ def __call__(self, other: Qobj) -> Qobj: if self.issuper: if other.isket: other = other.proj() - return vector_to_operator(self @ operator_to_vector(other)) + return qutip.vector_to_operator(self @ qutip.operator_to_vector(other)) return self.__matmul__(other) def __getstate__(self): @@ -591,7 +592,7 @@ def __and__(self, other: Qobj) -> Qobj: Syntax shortcut for tensor: A & B ==> tensor(A, B) """ - return tensor(self, other) + return qutip.tensor(self, other) def dag(self) -> Qobj: """Get the Hermitian adjoint of the quantum object.""" @@ -633,9 +634,9 @@ def dual_chan(self) -> Qobj: # is only valid for completely positive maps. if not self.iscp: raise ValueError("Dual channels are only implemented for CP maps.") - J = to_choi(self) + J = qutip.to_choi(self) tensor_idxs = enumerate_flat(J.dims) - J_dual = tensor_swap(J, *( + J_dual = qutip.tensor_swap(J, *( list(zip(tensor_idxs[0][1], tensor_idxs[0][0])) + list(zip(tensor_idxs[1][1], tensor_idxs[1][0])) )).trans() @@ -994,16 +995,16 @@ def unit( obj : :class:`.Qobj` Normalized quantum object. Will be the `self` object if in place. """ - norm = self.norm(norm=norm, kwargs=kwargs) + norm_ = self.norm(norm=norm, kwargs=kwargs) if inplace: - self.data = _data.mul(self.data, 1 / norm) - self._isherm = self._isherm if norm.imag == 0 else None + self.data = _data.mul(self.data, 1 / norm_) + self._isherm = self._isherm if norm_.imag == 0 else None self._isunitary = (self._isunitary - if abs(norm) - 1 < settings.core['atol'] + if abs(norm_) - 1 < settings.core['atol'] else None) out = self else: - out = self / norm + out = self / norm_ return out def ptrace(self, sel: int | list[int], dtype: LayerType = None) -> Qobj: @@ -1055,10 +1056,10 @@ def ptrace(self, sel: int | list[int], dtype: LayerType = None) -> Qobj: sel = [sel] if self.isoperket: dims = self.dims[0] - data = vector_to_operator(self).data + data = qutip.vector_to_operator(self).data elif self.isoperbra: dims = self.dims[1] - data = vector_to_operator(self.dag()).data + data = qutip.vector_to_operator(self.dag()).data elif self.issuper or self.isoper: dims = self.dims data = self.data @@ -1072,9 +1073,9 @@ def ptrace(self, sel: int | list[int], dtype: LayerType = None) -> Qobj: new_dims = [[dims[x] for x in sel]] * 2 if sel else None out = Qobj(new_data, dims=new_dims, copy=False) if self.isoperket: - return operator_to_vector(out) + return qutip.operator_to_vector(out) if self.isoperbra: - return operator_to_vector(out).dag() + return qutip.operator_to_vector(out).dag() return out def contract(self, inplace: bool = False) -> Qobj: @@ -1639,14 +1640,14 @@ def dnorm(self, B: Qobj = None) -> float: from this operator to B. """ - return mts.dnorm(self, B) + return qutip.dnorm(self, B) @property def ishp(self) -> bool: # FIXME: this needs to be cached in the same ways as isherm. if self.type in ["super", "oper"]: try: - J = to_choi(self) + J = qutip.to_choi(self) return J.isherm except: return False @@ -1661,7 +1662,7 @@ def iscp(self) -> bool: # We can test with either Choi or chi, since the basis # transformation between them is unitary and hence preserves # the CP and TP conditions. - J = self if self.superrep in ('choi', 'chi') else to_choi(self) + J = self if self.superrep in ('choi', 'chi') else qutip.to_choi(self) # If J isn't hermitian, then that could indicate either that J is not # normal, or is normal, but has complex eigenvalues. In either case, # it makes no sense to then demand that the eigenvalues be @@ -1679,7 +1680,7 @@ def istp(self) -> bool: if self.issuper and self.superrep in ('choi', 'chi'): qobj = self else: - qobj = to_choi(self) + qobj = qutip.to_choi(self) # Possibly collapse dims. if any([len(index) > 1 for super_index in qobj.dims @@ -1701,7 +1702,7 @@ def iscptp(self) -> bool: if not (self.issuper or self.isoper): return False reps = ('choi', 'chi') - q_oper = to_choi(self) if self.superrep not in reps else self + q_oper = qutip.to_choi(self) if self.superrep not in reps else self return q_oper.iscp and q_oper.istp @property @@ -1794,11 +1795,3 @@ def ptrace(Q: Qobj, sel: int | list[int]) -> Qobj: if not isinstance(Q, Qobj): raise TypeError("Input is not a quantum object") return Q.ptrace(sel) - - -# TRAILING IMPORTS -# We do a few imports here to avoid circular dependencies. -from qutip.core.superop_reps import to_choi -from qutip.core.superoperator import vector_to_operator, operator_to_vector -from qutip.core.tensor import tensor_swap, tensor -from qutip.core import metrics as mts diff --git a/qutip/core/superoperator.py b/qutip/core/superoperator.py index c493405ead..706c2826b7 100644 --- a/qutip/core/superoperator.py +++ b/qutip/core/superoperator.py @@ -9,6 +9,7 @@ import numpy as np from .qobj import Qobj +from .cy.qobjevo import QobjEvo from . import data as _data from .dimensions import Compound, SuperSpace, Space @@ -40,18 +41,18 @@ def liouvillian( @overload def liouvillian( - H: Qobj | "QobjEvo", - c_ops: list[Qobj | "QobjEvo"], + H: Qobj | QobjEvo, + c_ops: list[Qobj | QobjEvo], data_only: bool, chi: list[float] -) -> "QobjEvo": ... +) -> QobjEvo: ... def liouvillian( - H: Qobj | "QobjEvo" = None, - c_ops: list[Qobj | "QobjEvo"] = None, + H: Qobj | QobjEvo = None, + c_ops: list[Qobj | QobjEvo] = None, data_only: bool = False, chi: list[float] = None, -) -> Qobj | "QobjEvo": +) -> Qobj | QobjEvo: """Assembles the Liouvillian superoperator from a Hamiltonian and a ``list`` of collapse operators. @@ -149,18 +150,18 @@ def lindblad_dissipator( @overload def lindblad_dissipator( - a: Qobj | "QobjEvo", - b: Qobj | "QobjEvo", + a: Qobj | QobjEvo, + b: Qobj | QobjEvo, data_only: bool, chi: list[float] -) -> "QobjEvo": ... +) -> QobjEvo: ... def lindblad_dissipator( - a: Qobj | "QobjEvo", - b: Qobj | "QobjEvo" = None, + a: Qobj | QobjEvo, + b: Qobj | QobjEvo = None, data_only: bool = False, chi: list[float] = None, -) -> Qobj | "QobjEvo": +) -> Qobj | QobjEvo: """ Lindblad dissipator (generalized) for a single pair of collapse operators (a, b), or for a single collapse operator (a) when b is not specified: @@ -297,7 +298,7 @@ def stack_columns(matrix: QobjOrArray) -> QobjOrArray: def unstack_columns( vector: QobjOrArray, - shape: typle[int, int] = None, + shape: tuple[int, int] = None, ) -> QobjOrArray: """ Unstack the columns in a data-layer type back into a 2D shape, useful for @@ -343,7 +344,7 @@ def stacked_index(size, row, col): return row + size*col -AnyQobj = TypeVar("AnyQobj", Qobj, "QobjEvo") +AnyQobj = TypeVar("AnyQobj", Qobj, QobjEvo) @_map_over_compound_operators @@ -404,10 +405,10 @@ def _drop_projected_dims(dims): @overload -sprepost(A: Qobj, B: Qobj) -> Qobj: ... +def sprepost(A: Qobj, B: Qobj) -> Qobj: ... @overload -sprepost(A: Qobj | "QobjEvo", B: Qobj | "QobjEvo") -> "QobjEvo": ... +def sprepost(A: Qobj | QobjEvo, B: Qobj | QobjEvo) -> QobjEvo: ... def sprepost(A, B): """ diff --git a/qutip/core/tensor.py b/qutip/core/tensor.py index ec8ed0af37..013fb75589 100644 --- a/qutip/core/tensor.py +++ b/qutip/core/tensor.py @@ -9,8 +9,11 @@ import numpy as np from functools import partial +from typing import TypeVar, overload + from .operators import qeye from .qobj import Qobj +from .cy.qobjevo import QobjEvo from .superoperator import operator_to_vector, reshuffle from .dimensions import ( flatten, enumerate_flat, unflatten, deep_remove, dims_to_tensor_shape, @@ -18,6 +21,7 @@ ) from . import data as _data from .. import settings +from ..typing import LayerType class _reverse_partial_tensor: @@ -33,9 +37,9 @@ def __call__(self, op): def tensor(*args: Qobj) -> Qobj: ... @overload -def tensor(*args: Qobj | "QobjEvo") -> "QobjEvo": ... +def tensor(*args: Qobj | QobjEvo) -> QobjEvo: ... -def tensor(*args: Qobj | "QobjEvo") -> Qobj | "QobjEvo": +def tensor(*args: Qobj | QobjEvo) -> Qobj | QobjEvo: """Calculates the tensor product of input operators. Parameters @@ -112,7 +116,13 @@ def tensor(*args: Qobj | "QobjEvo") -> Qobj | "QobjEvo": copy=False) -def super_tensor(*args): +@overload +def super_tensor(*args: Qobj) -> Qobj: ... + +@overload +def super_tensor(*args: Qobj | QobjEvo) -> QobjEvo: ... + +def super_tensor(*args: Qobj | QobjEvo) -> Qobj | QobjEvo: """ Calculate the tensor product of input superoperators, by tensoring together the underlying Hilbert spaces on which each vectorized operator acts. @@ -180,6 +190,12 @@ def _isbralike(q): return q.isbra or q.isoperbra +@overload +def composite(*args: Qobj) -> Qobj: ... + +@overload +def composite(*args: Qobj | QobjEvo) -> QobjEvo: ... + def composite(*args): """ Given two or more operators, kets or bras, returns the Qobj @@ -251,13 +267,18 @@ def _tensor_contract_dense(arr, *pairs): return arr -def tensor_swap(q_oper, *pairs): +def tensor_swap(q_oper: Qobj, *pairs: tuple[int, int]) -> Qobj: """Transposes one or more pairs of indices of a Qobj. - Note that this uses dense representations and thus - should *not* be used for very large Qobjs. + + .. note:: + + Note that this uses dense representations and thus + should *not* be used for very large Qobjs. Parameters ---------- + q_oper : Qobj + Operator to swap dims. pairs : tuple One or more tuples ``(i, j)`` indicating that the @@ -290,10 +311,13 @@ def tensor_swap(q_oper, *pairs): return Qobj(data, dims=dims, superrep=q_oper.superrep, copy=False) -def tensor_contract(qobj, *pairs): +def tensor_contract(qobj: Qobj, *pairs: tuple[int, int]) -> Qobj: """Contracts a qobj along one or more index pairs. - Note that this uses dense representations and thus - should *not* be used for very large Qobjs. + + .. note:: + + Note that this uses dense representations and thus + should *not* be used for very large Qobjs. Parameters ---------- @@ -420,7 +444,15 @@ def _targets_to_list(targets, oper=None, N=None): return targets -def expand_operator(oper, dims, targets, dtype=None): +QobjOrQobjEvo = TypeVar("QobjOrQobjEvo", Qobj, QobjEvo) + + +def expand_operator( + oper: QobjOrQobjEvo, + dims: list[int], + targets: int, + dtype: LayerType = None +) -> QobjOrQobjEvo: """ Expand an operator to one that acts on a system with desired dimensions. e.g. From bbf76e8dff1ba1b66f6c95525b7efc61dbc4f757 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 5 Jul 2024 16:45:47 -0400 Subject: [PATCH 269/305] More types in core --- qutip/core/blochredfield.py | 29 ++++++++++---- qutip/core/dimensions.py | 36 +++++++++--------- qutip/core/expect.py | 53 ++++++++++++++++++-------- qutip/core/options.py | 72 +++++++++++++++++++++++++++++++---- qutip/core/subsystem_apply.py | 7 +++- qutip/settings.py | 34 ++++++++--------- 6 files changed, 165 insertions(+), 66 deletions(-) diff --git a/qutip/core/blochredfield.py b/qutip/core/blochredfield.py index b3c0509572..00237ab88a 100644 --- a/qutip/core/blochredfield.py +++ b/qutip/core/blochredfield.py @@ -7,14 +7,20 @@ from ._brtools import SpectraCoefficient, _EigenBasisTransform from .cy.coefficient import InterCoefficient, Coefficient from ._brtensor import _BlochRedfieldElement - +from ..typing import CoeffProtocol __all__ = ['bloch_redfield_tensor', 'brterm'] -def bloch_redfield_tensor(H, a_ops, c_ops=[], sec_cutoff=0.1, - fock_basis=False, sparse_eigensolver=False, - br_dtype='sparse'): +def bloch_redfield_tensor( + H: Qobj | QobjEvo, + a_ops: list[tuple[Qobj | QobjEvo, Coefficient | str | CoeffProtocol]], + c_ops: list[Qobj | QobjEvo] = None, + sec_cutoff: float = 0.1, + fock_basis: bool = False, + sparse_eigensolver: bool = False, + br_dtype: str = 'sparse', +): """ Calculates the Bloch-Redfield tensor for a system given a set of operators and corresponding spectral functions that describes the @@ -46,10 +52,10 @@ def bloch_redfield_tensor(H, a_ops, c_ops=[], sec_cutoff=0.1, .. code-block:: a_ops = [ - (a+a.dag(), ('w>0', args={"w": 0})), + (a+a.dag(), coefficient('w>0', args={"w": 0})), (QobjEvo(a+a.dag()), 'w > exp(-t)'), (QobjEvo([b+b.dag(), lambda t: ...]), lambda w: ...)), - (c+c.dag(), SpectraCoefficient(coefficient(array, tlist=ws))), + (c+c.dag(), SpectraCoefficient(coefficient(ws, tlist=ts))), ] @@ -102,8 +108,15 @@ def bloch_redfield_tensor(H, a_ops, c_ops=[], sec_cutoff=0.1, return R, H_transform.as_Qobj() -def brterm(H, a_op, spectra, sec_cutoff=0.1, - fock_basis=False, sparse_eigensolver=False, br_dtype='sparse'): +def brterm( + H: Qobj | QobjEvo, + a_op: Qobj | QobjEvo, + spectra: Coefficient | CoeffProtocol | str, + sec_cutoff: float = 0.1, + fock_basis: bool = False, + sparse_eigensolver: bool = False, + br_dtype: str = 'sparse', +): """ Calculates the contribution of one coupling operator to the Bloch-Redfield tensor. diff --git a/qutip/core/dimensions.py b/qutip/core/dimensions.py index e92e8d2a45..e1834e9f2b 100644 --- a/qutip/core/dimensions.py +++ b/qutip/core/dimensions.py @@ -353,13 +353,13 @@ def _frozen(*args, **kwargs): class MetaSpace(type): - def __call__(cls, *args: SpaceLike, rep: str = None) -> Space: + def __call__(cls, *args: SpaceLike, rep: str = None) -> "Space": """ Select which subclass is instantiated. """ if cls is Space and len(args) == 1 and isinstance(args[0], list): # From a list of int. - return cls.from_list(*args, rep=rep) + return cls.from_list(args[0], rep=rep) elif len(args) == 1 and isinstance(args[0], Space): # Already a Space return args[0] @@ -405,7 +405,7 @@ def from_list( cls, list_dims: list[int] | list[list[int]], rep: str = None - ) -> Space: + ) -> "Space": if len(list_dims) == 0: raise ValueError("Empty list can't be used as dims.") elif ( @@ -514,7 +514,7 @@ def remove(self, idx: int): """ raise RuntimeError("Cannot delete a flat space.") - def replace(self, idx: int, new: int) -> Space: + def replace(self, idx: int, new: int) -> "Space": """ Reshape a Space from a Dimensons or complex Space. @@ -526,10 +526,10 @@ def replace(self, idx: int, new: int) -> Space: ) return Space(new) - def replace_superrep(self, super_rep: str) -> Space: + def replace_superrep(self, super_rep: str) -> "Space": return self - def scalar_like(self) -> SpaceLike: + def scalar_like(self) -> "Space": return Field() @@ -575,7 +575,7 @@ def replace(self, idx: int, new: int) -> Space: class Compound(Space): _stored_dims = {} - def __init__(self, *spaces): + def __init__(self, *spaces: Space): spaces_ = [] if len(spaces) <= 1: raise ValueError("Compound need multiple space to join.") @@ -618,7 +618,7 @@ def __repr__(self) -> str: def as_list(self) -> list[int]: return sum([space.as_list() for space in self.spaces], []) - def dims2idx(self, dims-> list[int]) -> int: + def dims2idx(self, dims: list[int]) -> int: if len(dims) != len(self.spaces): raise ValueError("Length of supplied dims does not match the number of subspaces.") pos = 0 @@ -674,13 +674,13 @@ def replace_superrep(self, super_rep: str) -> Space: ) def scalar_like(self) -> Space: - return Compound([space.scalar_like() for space in self.spaces]) + return Space([space.scalar_like() for space in self.spaces]) class SuperSpace(Space): _stored_dims = {} - def __init__(self, oper: Dimensions, rep: str = 'super'): + def __init__(self, oper: "Dimensions", rep: str = 'super'): self.oper = oper self.superrep = rep self.size = oper.shape[0] * oper.shape[1] @@ -742,7 +742,7 @@ def scalar_like(self) -> Space: class MetaDims(type): - def __call__(cls, *args: DimensionLike, rep: str = None) -> Dimensions: + def __call__(cls, *args: DimensionLike, rep: str = None) -> "Dimensions": if len(args) == 1 and isinstance(args[0], Dimensions): return args[0] elif len(args) == 1 and len(args[0]) == 2: @@ -807,7 +807,7 @@ def __init__(self, from_: Space, to_: Space): self.superrep = 'mixed' self.__setitem__ = _frozen - def __eq__(self, other: Dimensions) -> bool: + def __eq__(self, other: "Dimensions") -> bool: if isinstance(other, Dimensions): return ( self is other @@ -818,7 +818,7 @@ def __eq__(self, other: Dimensions) -> bool: ) return NotImplemented - def __ne__(self, other: Dimensions) -> bool: + def __ne__(self, other: "Dimensions") -> bool: if isinstance(other, Dimensions): return not ( self is other @@ -829,7 +829,7 @@ def __ne__(self, other: Dimensions) -> bool: ) return NotImplemented - def __matmul__(self, other: Dimensions) -> Dimensions: + def __matmul__(self, other: "Dimensions") -> "Dimensions": if self.from_ != other.to_: raise TypeError(f"incompatible dimensions {self} and {other}") args = other.from_, self.to_ @@ -913,7 +913,7 @@ def _get_tensor_perm(self): np.argsort(stepr)[::-1] + len(stepl) ])) - def remove(self, idx: int | list[int]) -> Dimensions: + def remove(self, idx: int | list[int]) -> "Dimensions": """ Remove a Space from a Dimensons or complex Space. @@ -932,7 +932,7 @@ def remove(self, idx: int | list[int]) -> Dimensions: self.to_.remove(idx_to), ) - def replace(self, idx: int, new: int) -> Dimensions: + def replace(self, idx: int, new: int) -> "Dimensions": """ Reshape a Space from a Dimensons or complex Space. @@ -948,7 +948,7 @@ def replace(self, idx: int, new: int) -> Dimensions: return Dimensions(new_from, new_to) - def replace_superrep(self, super_rep: str) -> Dimensions: + def replace_superrep(self, super_rep: str) -> "Dimensions": if not self.issuper and super_rep is not None: raise TypeError("Can't set a superrep of a non super object.") return Dimensions( @@ -956,5 +956,5 @@ def replace_superrep(self, super_rep: str) -> Dimensions: self.to_.replace_superrep(super_rep) ) - def scalar_like(self) -> Dimensions: + def scalar_like(self) -> "Dimensions": return Dimensions([self.to_.scalar_like(), self.from_.scalar_like()]) diff --git a/qutip/core/expect.py b/qutip/core/expect.py index 74c957e6ac..c2ac3da948 100644 --- a/qutip/core/expect.py +++ b/qutip/core/expect.py @@ -1,12 +1,34 @@ __all__ = ['expect', 'variance'] import numpy as np +from typing import overload, Sequence from .qobj import Qobj from . import data as _data from ..settings import settings +@overload +def expect(oper: Qobj, state: Qobj) -> complex: ... + +@overload +def expect( + oper: Qobj, + state: Qobj | Sequence[Qobj], +) -> np.typing.NDArray[complex]: ... + +@overload +def expect( + oper: Qobj | Sequence[Qobj], + state: Qobj, +) -> list[complex]: ... + +@overload +def expect( + oper: Qobj | Sequence[Qobj], + state: Qobj | Sequence[Qobj] +) -> list[np.typing.NDArray[complex]]: ... + def expect(oper, state): """ Calculate the expectation value for operator(s) and state(s). The @@ -16,17 +38,18 @@ def expect(oper, state): Parameters ---------- - oper : qobj/array-like + oper : qobj / list of Qobj A single or a `list` of operators for expectation value. - state : qobj/array-like + state : qobj / list of Qobj A single or a `list` of quantum states or density matrices. Returns ------- - expt : float/complex/array-like - Expectation value. ``real`` if ``oper`` is Hermitian, ``complex`` - otherwise. A (nested) array of expectaction values if ``state`` or + expt : float / complex / list / array + Expectation value(s). ``real`` if ``oper`` is Hermitian, ``complex`` + otherwise. If multiple ``oper`` are passed, a list of array + A (nested) array of expectaction values if ``state`` or ``oper`` are arrays. Examples @@ -38,16 +61,10 @@ def expect(oper, state): if isinstance(state, Qobj) and isinstance(oper, Qobj): return _single_qobj_expect(oper, state) - elif isinstance(oper, (list, np.ndarray)): - if isinstance(state, Qobj): - dtype = np.complex128 - if all(op.isherm for op in oper) and (state.isket or state.isherm): - dtype = np.float64 - return np.array([_single_qobj_expect(op, state) for op in oper], - dtype=dtype) + elif isinstance(oper, Sequence): return [expect(op, state) for op in oper] - elif isinstance(state, (list, np.ndarray)): + elif isinstance(state, Sequence): dtype = np.complex128 if oper.isherm and all(op.isherm or op.isket for op in state): dtype = np.float64 @@ -81,16 +98,22 @@ def _single_qobj_expect(oper, state): return out +@overload +def variance(oper: Qobj, state: Qobj) -> complex: + +@overload +def variance(oper: Qobj, state: list[Qobj]) -> np.typing.NDArray[complex]: + def variance(oper, state): """ Variance of an operator for the given state vector or density matrix. Parameters ---------- - oper : qobj + oper : Qobj Operator for expectation value. - state : qobj/list + state : Qobj / list of Qobj A single or ``list`` of quantum states or density matrices.. Returns diff --git a/qutip/core/options.py b/qutip/core/options.py index 683572ed61..640d4f7f16 100644 --- a/qutip/core/options.py +++ b/qutip/core/options.py @@ -1,4 +1,6 @@ from ..settings import settings +from typing import overload, Literal, Any +import types __all__ = ["CoreOptions"] @@ -10,7 +12,8 @@ class QutipOptions: Define basic method to wrap an ``options`` dict. Default options are in a class _options dict. """ - _options = {} + + _options: dict[str, Any] = {} _settings_name = None # Where the default is in settings def __init__(self, **options): @@ -20,24 +23,24 @@ def __init__(self, **options): if options: raise KeyError(f"Options {set(options)} are not supported.") - def __contains__(self, key): + def __contains__(self, key: str) -> bool: return key in self.options - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: # Let the dict catch the KeyError return self.options[key] - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: # Let the dict catch the KeyError self.options[key] = value - def __repr__(self, full=True): + def __repr__(self, full: bool = True) -> str: out = [f"<{self.__class__.__name__}("] for key, value in self.options.items(): if full or value != self._options[key]: out += [f" '{key}': {repr(value)},"] out += [")>"] - if len(out)-2: + if len(out) - 2: return "\n".join(out) else: return "".join(out) @@ -46,7 +49,12 @@ def __enter__(self): self._backup = getattr(settings, self._settings_name) setattr(settings, self._settings_name, self) - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: types.TracebackType | None, + ) -> None: setattr(settings, self._settings_name, self._backup) @@ -110,6 +118,7 @@ class CoreOptions(QutipOptions): known to ``qutip.data.to`` is accepted. When ``None``, these functions will default to a sensible data type. """ + _options = { # use auto tidyup "auto_tidyup": True, @@ -131,6 +140,55 @@ class CoreOptions(QutipOptions): } _settings_name = "core" + @overload + def __getitem__( + self, + key: Literal["auto_tidyup", "auto_tidyup_dims", "auto_real_casting"], + ) -> bool: ... + + @overload + def __getitem__( + self, key: Literal["atol", "rtol", "auto_tidyup_atol"] + ) -> float: ... + + @overload + def __getitem__( + self, key: Literal["function_coefficient_style"] + ) -> str: ... + + @overload + def __getitem__(self, key: Literal["default_dtype"]) -> str | None: ... + + def __getitem__(self, key: str) -> Any: + # Let the dict catch the KeyError + return self.options[key] + + @overload + def __setitem__( + self, + key: Literal["auto_tidyup", "auto_tidyup_dims", "auto_real_casting"], + value: bool, + ) -> None: ... + + @overload + def __setitem__( + self, key: Literal["atol", "rtol", "auto_tidyup_atol"], value: float + ) -> None: ... + + @overload + def __setitem__( + self, key: Literal["function_coefficient_style"], value: str + ) -> None: ... + + @overload + def __setitem__( + self, key: Literal["default_dtype"], value: str | None + ) -> None: ... + + def __setitem__(self, key: str, value: Any) -> None: + # Let the dict catch the KeyError + self.options[key] = value + # Creating the instance of core options to use everywhere. settings.core = CoreOptions() diff --git a/qutip/core/subsystem_apply.py b/qutip/core/subsystem_apply.py index e7bea30a2f..123b6ea2d8 100644 --- a/qutip/core/subsystem_apply.py +++ b/qutip/core/subsystem_apply.py @@ -13,7 +13,12 @@ from . import data as _data -def subsystem_apply(state, channel, mask, reference=False): +def subsystem_apply( + state: Qobj, + channel: Qobj, + mask: list[bool], + reference: bool=False +)-> Qobj: """ Returns the result of applying the propagator `channel` to the subsystems indicated in `mask`, which comprise the density operator diff --git a/qutip/settings.py b/qutip/settings.py index bcd9ee09a9..884c9f3de0 100644 --- a/qutip/settings.py +++ b/qutip/settings.py @@ -34,7 +34,7 @@ def _in_libaries(name): return blas -def available_cpu_count(): +def available_cpu_count() -> int: """ Get the number of cpus. It tries to only get the number available to qutip. @@ -135,19 +135,19 @@ def __init__(self): self._colorblind_safe = False @property - def has_mkl(self): + def has_mkl(self) -> bool: """ Whether qutip found an mkl installation. """ return self.mkl_lib is not None @property - def mkl_lib(self): + def mkl_lib(self) -> str | None: """ Location of the mkl installation. """ if self._mkl_lib == "": self._mkl_lib = _find_mkl() return _find_mkl() @property - def ipython(self): + def ipython(self) -> bool: """ Whether qutip is running in ipython. """ try: __IPYTHON__ @@ -156,7 +156,7 @@ def ipython(self): return False @property - def eigh_unsafe(self): + def eigh_unsafe(self) -> bool: """ Whether `eigh` call is reliable. Some implementation of blas have some issues on some OS. @@ -175,7 +175,7 @@ def eigh_unsafe(self): ) @property - def tmproot(self): + def tmproot(self) -> str: """ Location in which qutip place cython string coefficient folders. The default is "$HOME/.qutip". @@ -184,13 +184,13 @@ def tmproot(self): return self._tmproot @tmproot.setter - def tmproot(self, root): + def tmproot(self, root: str) -> None: if not os.path.exists(root): os.mkdir(root) self._tmproot = root @property - def coeffroot(self): + def coeffroot(self) -> str: """ Location in which qutip save cython string coefficient files. Usually "{qutip.settings.tmproot}/qutip_coeffs_X.X". @@ -199,7 +199,7 @@ def coeffroot(self): return self._coeffroot @coeffroot.setter - def coeffroot(self, root): + def coeffroot(self, root: str) -> None: if not os.path.exists(root): os.mkdir(root) if root not in sys.path: @@ -207,18 +207,18 @@ def coeffroot(self, root): self._coeffroot = root @property - def coeff_write_ok(self): + def coeff_write_ok(self) -> bool: """ Whether qutip has write acces to ``qutip.settings.coeffroot``.""" return os.access(self.coeffroot, os.W_OK) @property - def _has_openmp(self): + def _has_openmp(self) -> bool: return False # We keep this as a reminder for when openmp is restored: see Pull #652 # os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' @property - def idxint_size(self): + def idxint_size(self) -> int: """ Integer type used by ``CSR`` data. Sparse ``CSR`` matrices can contain at most ``2**idxint_size`` @@ -228,7 +228,7 @@ def idxint_size(self): return data.base.idxint_size @property - def num_cpus(self): + def num_cpus(self) -> int: """ Number of cpu detected. Use the solver options to control the number of cpus used. @@ -241,7 +241,7 @@ def num_cpus(self): return num_cpus @property - def colorblind_safe(self): + def colorblind_safe(self) -> bool: """ Allow for a colorblind mode that uses different colormaps and plotting options by default. @@ -249,10 +249,10 @@ def colorblind_safe(self): return self._colorblind_safe @colorblind_safe.setter - def colorblind_safe(self, value): + def colorblind_safe(self, value: bool) -> None: self._colorblind_safe = value - def __str__(self): + def __str__(self) -> str: lines = ["Qutip settings:"] for attr in self.__dir__(): if not attr.startswith('_') and attr not in ["core", "compile"]: @@ -260,7 +260,7 @@ def __str__(self): lines.append(f" compile: {self.compile.__repr__(full=False)}") return '\n'.join(lines) - def __repr__(self): + def __repr__(self) -> str: return self.__str__() From 03b6f7e3e739836b1b0c8f6a797b94b87db64e3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:26:51 +0000 Subject: [PATCH 270/305] Bump certifi from 2023.7.22 to 2024.7.4 in /doc Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.7.22 to 2024.7.4. - [Commits](https://github.com/certifi/python-certifi/compare/2023.07.22...2024.07.04) --- updated-dependencies: - dependency-name: certifi dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- doc/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index c0b500a983..4c64df4724 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,7 @@ alabaster==0.7.13 Babel==2.12.1 backcall==0.2.0 -certifi==2023.7.22 +certifi==2024.7.4 chardet==4.0.0 cycler==0.10.0 Cython==3.0.8 From dde50cb7a9ac542c3429ec4aca0bbf54bb4a8c1f Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Mon, 8 Jul 2024 20:29:42 +0530 Subject: [PATCH 271/305] fix expm to work with jax --- qutip/core/qobj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index d8c41588ec..e5ec8362f9 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -822,7 +822,7 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """ if not self._dims.issquare: raise TypeError("expm is only valid for square operators") - return Qobj(_data.expm(self._data, dtype=dtype), + return Qobj(_data.expm(self._data, dtype=self.dtype), dims=self._dims, isherm=self._isherm, copy=False) From 7817085555b2709e837b4b9d8c9cc64c302f3502 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Mon, 8 Jul 2024 20:44:42 +0530 Subject: [PATCH 272/305] add towncrier entry --- doc/changes/2484.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2484.bugfix diff --git a/doc/changes/2484.bugfix b/doc/changes/2484.bugfix new file mode 100644 index 0000000000..bb2559b969 --- /dev/null +++ b/doc/changes/2484.bugfix @@ -0,0 +1 @@ +This change makes expm, cosm, sinm work with jax. From 0601f7f8b1416ebbfb07dfea0744e3a09c81612a Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 8 Jul 2024 11:28:53 -0400 Subject: [PATCH 273/305] Type in expect --- qutip/core/expect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutip/core/expect.py b/qutip/core/expect.py index c2ac3da948..d4a5447e2f 100644 --- a/qutip/core/expect.py +++ b/qutip/core/expect.py @@ -99,10 +99,10 @@ def _single_qobj_expect(oper, state): @overload -def variance(oper: Qobj, state: Qobj) -> complex: +def variance(oper: Qobj, state: Qobj) -> complex: ... @overload -def variance(oper: Qobj, state: list[Qobj]) -> np.typing.NDArray[complex]: +def variance(oper: Qobj, state: list[Qobj]) -> np.typing.NDArray[complex]: ... def variance(oper, state): """ From bc5c099401a85d1a70dde1e0076fbdfd2a4c65e1 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 8 Jul 2024 11:39:14 -0400 Subject: [PATCH 274/305] Add types in super_reps --- qutip/core/superop_reps.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qutip/core/superop_reps.py b/qutip/core/superop_reps.py index 9a5d7222c3..4413cfafb5 100644 --- a/qutip/core/superop_reps.py +++ b/qutip/core/superop_reps.py @@ -81,7 +81,7 @@ def _nq(dims): return nq -def isqubitdims(dims): +def isqubitdims(dims: list[list[int]] | list[list[list[int]]]) -> bool: """ Checks whether all entries in a dims list are integer powers of 2. @@ -138,7 +138,7 @@ def _choi_to_kraus(q_oper, tol=1e-9): # Individual conversions from Kraus operators are public because the output # list of Kraus operators is not itself a quantum object. -def kraus_to_choi(kraus_ops): +def kraus_to_choi(kraus_ops: list[Qobj]) -> Qobj: r""" Convert a list of Kraus operators into Choi representation of the channel. @@ -176,7 +176,7 @@ def kraus_to_choi(kraus_ops): return Qobj(choi_array, choi_dims, superrep="choi", copy=False) -def kraus_to_super(kraus_list): +def kraus_to_super(kraus_list: list[Qobj]) -> Qobj: """ Convert a list of Kraus operators to a superoperator. @@ -346,7 +346,7 @@ def _choi_to_stinespring(q_oper, threshold=1e-10): return A, B -def to_choi(q_oper): +def to_choi(q_oper: Qobj) -> Qobj: """ Converts a Qobj representing a quantum map to the Choi representation, such that the trace of the returned operator is equal to the dimension @@ -389,7 +389,7 @@ def to_choi(q_oper): ) -def to_chi(q_oper): +def to_chi(q_oper: Qobj) -> Qobj: """ Converts a Qobj representing a quantum map to a representation as a chi (process) matrix in the Pauli basis, such that the trace of the returned @@ -432,7 +432,7 @@ def to_chi(q_oper): ) -def to_super(q_oper): +def to_super(q_oper: Qobj) -> Qobj: """ Converts a Qobj representing a quantum map to the supermatrix (Liouville) representation. @@ -476,7 +476,7 @@ def to_super(q_oper): ) -def to_kraus(q_oper, tol=1e-9): +def to_kraus(q_oper: Qobj, tol: float=1e-9) -> list[Qobj]: """ Converts a Qobj representing a quantum map to a list of quantum objects, each representing an operator in the Kraus decomposition of the given map. @@ -515,7 +515,7 @@ def to_kraus(q_oper, tol=1e-9): ) -def to_stinespring(q_oper, threshold=1e-10): +def to_stinespring(q_oper: Qobj, threshold: float=1e-10) -> tuple[Qobj, Qobj]: r""" Converts a Qobj representing a quantum map :math:`\Lambda` to a pair of partial isometries ``A`` and ``B`` such that From 634896a972d82294a8b36da95513e5d9a14826e4 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 8 Jul 2024 13:02:38 -0400 Subject: [PATCH 275/305] Update result format in tests --- qutip/tests/core/test_expect.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/qutip/tests/core/test_expect.py b/qutip/tests/core/test_expect.py index f461a36f59..33dd1b5178 100644 --- a/qutip/tests/core/test_expect.py +++ b/qutip/tests/core/test_expect.py @@ -92,11 +92,10 @@ def test_operator_by_basis(self, operator, state, expected): def test_broadcast_operator_list(self, operators, state, expected): result = qutip.expect(operators, state) - expected_dtype = (np.float64 if all(op.isherm for op in operators) - else np.complex128) - assert isinstance(result, np.ndarray) - assert result.dtype == expected_dtype - assert list(result) == list(expected) + assert len(result) == len(operators) + for part, operator, expected_part in zip(result, operators, expected): + assert isinstance(part, float if operator.isherm else complex) + assert part == expected_part def test_broadcast_state_list(self, operator, states, expected): result = qutip.expect(operator, states) From b5738eca8bfd0e2a5ff5dbccd7ae691dc147d18f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 8 Jul 2024 13:28:41 -0400 Subject: [PATCH 276/305] Finish brtensor types --- qutip/core/blochredfield.py | 50 ++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/qutip/core/blochredfield.py b/qutip/core/blochredfield.py index 00237ab88a..c76f8a33dc 100644 --- a/qutip/core/blochredfield.py +++ b/qutip/core/blochredfield.py @@ -1,8 +1,9 @@ import os import inspect import numpy as np -from qutip.settings import settings as qset +from typing import overload +from qutip.settings import settings as qset from . import Qobj, QobjEvo, liouvillian, coefficient, sprepost from ._brtools import SpectraCoefficient, _EigenBasisTransform from .cy.coefficient import InterCoefficient, Coefficient @@ -12,6 +13,18 @@ __all__ = ['bloch_redfield_tensor', 'brterm'] +@overload +def bloch_redfield_tensor( + H: Qobj, + a_ops: list[tuple[Qobj, Coefficient | str | CoeffProtocol]], + c_ops: list[Qobj | QobjEvo] = None, + sec_cutoff: float = 0.1, + fock_basis: bool = False, + sparse_eigensolver: bool = False, + br_dtype: str = 'sparse', +) -> Qobj: ... + +@overload def bloch_redfield_tensor( H: Qobj | QobjEvo, a_ops: list[tuple[Qobj | QobjEvo, Coefficient | str | CoeffProtocol]], @@ -20,7 +33,17 @@ def bloch_redfield_tensor( fock_basis: bool = False, sparse_eigensolver: bool = False, br_dtype: str = 'sparse', -): +) -> QobjEvo: ... + +def bloch_redfield_tensor( + H: Qobj | QobjEvo, + a_ops: list[tuple[Qobj | QobjEvo, Coefficient | str | CoeffProtocol]], + c_ops: list[Qobj | QobjEvo] = None, + sec_cutoff: float = 0.1, + fock_basis: bool = False, + sparse_eigensolver: bool = False, + br_dtype: str = 'sparse', +) -> Qobj | QobjEvo: """ Calculates the Bloch-Redfield tensor for a system given a set of operators and corresponding spectral functions that describes the @@ -107,6 +130,27 @@ def bloch_redfield_tensor( False, br_dtype=br_dtype)[0] return R, H_transform.as_Qobj() +@overload +def brterm( + H: Qobj, + a_op: Qobj, + spectra: Coefficient | CoeffProtocol | str, + sec_cutoff: float = 0.1, + fock_basis: bool = False, + sparse_eigensolver: bool = False, + br_dtype: str = 'sparse', +) -> Qobj: ... + +@overload +def brterm( + H: Qobj | QobjEvo, + a_op: Qobj | QobjEvo, + spectra: Coefficient | CoeffProtocol | str, + sec_cutoff: float = 0.1, + fock_basis: bool = False, + sparse_eigensolver: bool = False, + br_dtype: str = 'sparse', +) -> QobjEvo: ... def brterm( H: Qobj | QobjEvo, @@ -116,7 +160,7 @@ def brterm( fock_basis: bool = False, sparse_eigensolver: bool = False, br_dtype: str = 'sparse', -): +) -> Qobj | QobjEvo: """ Calculates the contribution of one coupling operator to the Bloch-Redfield tensor. From 0e68509decb484dbb13e52deee36b5b739274c56 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 8 Jul 2024 13:57:50 -0400 Subject: [PATCH 277/305] Remove duplicated tests --- qutip/tests/core/data/test_convert.py | 2 +- qutip/tests/core/test_superop_reps.py | 34 --------------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/qutip/tests/core/data/test_convert.py b/qutip/tests/core/data/test_convert.py index 917003dced..0cbb7aaf9c 100644 --- a/qutip/tests/core/data/test_convert.py +++ b/qutip/tests/core/data/test_convert.py @@ -68,7 +68,7 @@ def test_converters(from_, base, to_, dtype): dtype_types = list(data.to._str2type.values()) + list(data.to.dtypes) @pytest.mark.parametrize(['input', 'type_'], zip(dtype_names, dtype_types), ids=[str(dtype) for dtype in dtype_names]) -def test_parse_error(input, type_): +def test_parse(input, type_): assert data.to.parse(input) is type_ diff --git a/qutip/tests/core/test_superop_reps.py b/qutip/tests/core/test_superop_reps.py index 80da2bd821..7190d0fe8c 100644 --- a/qutip/tests/core/test_superop_reps.py +++ b/qutip/tests/core/test_superop_reps.py @@ -175,39 +175,6 @@ def test_random_iscptp(self, superoperator): assert superoperator.iscptp assert superoperator.ishp - @pytest.mark.parametrize(['qobj', 'hp', 'cp', 'tp'], [ - pytest.param(sprepost(destroy(2), create(2)), True, True, False), - pytest.param(sprepost(destroy(2), destroy(2)), False, False, False), - pytest.param(qeye(2), True, True, True), - pytest.param(sigmax(), True, True, True), - pytest.param(tensor(sigmax(), qeye(2)), True, True, True), - pytest.param(0.5 * (to_super(tensor(sigmax(), qeye(2))) - + to_super(tensor(qeye(2), sigmay()))), - True, True, True, - id="linear combination of bipartite unitaries"), - pytest.param(Qobj(swap(), dims=[[[2],[2]]]*2, superrep='choi'), - True, False, True, - id="partial transpose map"), - pytest.param(Qobj(qeye(4)*0.9, dims=[[[2],[2]]]*2), True, True, False, - id="subnormalized map"), - pytest.param(basis(2, 0), False, False, False, id="ket"), - ]) - def test_known_iscptp(self, qobj, hp, cp, tp): - """ - Superoperator: ishp, iscp, istp and iscptp known cases. - """ - assert qobj.ishp == hp - assert qobj.iscp == cp - assert qobj.istp == tp - assert qobj.iscptp == (cp and tp) - - def test_choi_tr(self): - """ - Superoperator: Trace returned by to_choi matches docstring. - """ - for dims in range(2, 5): - assert abs(to_choi(identity(dims)).tr() - dims) < tol - # Conjugation by a creation operator a = create(2).dag() @@ -244,7 +211,6 @@ def test_choi_tr(self): pytest.param(ptr_swap, True, False, True, id="partial transpose map"), pytest.param(subnorm_map, True, True, False, id="subnorm map"), pytest.param(basis(2), False, False, False, id="not an operator"), - ]) def test_known_iscptp(self, qobj, shouldhp, shouldcp, shouldtp): """ From d2ac278f59bc79fea1f799839f1965d0741e52dc Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 9 Jul 2024 09:10:35 -0400 Subject: [PATCH 278/305] Fix bloch_redfield_tensor types --- qutip/core/blochredfield.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/blochredfield.py b/qutip/core/blochredfield.py index c76f8a33dc..0d76c635c7 100644 --- a/qutip/core/blochredfield.py +++ b/qutip/core/blochredfield.py @@ -17,7 +17,7 @@ def bloch_redfield_tensor( H: Qobj, a_ops: list[tuple[Qobj, Coefficient | str | CoeffProtocol]], - c_ops: list[Qobj | QobjEvo] = None, + c_ops: list[Qobj] = None, sec_cutoff: float = 0.1, fock_basis: bool = False, sparse_eigensolver: bool = False, From 91225c3b9f3abf1f8ce0feb1c4ff10b2eb8f9c4f Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Wed, 10 Jul 2024 18:53:04 +0530 Subject: [PATCH 279/305] add if for csr --- qutip/core/qobj.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index e5ec8362f9..b75deb4883 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -822,6 +822,11 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """ if not self._dims.issquare: raise TypeError("expm is only valid for square operators") + if isinstance(self.data, _data.CSR): + return Qobj(_data.expm(self._data, dtype=dtype), + dims=self._dims, + isherm=self._isherm, + copy=False) return Qobj(_data.expm(self._data, dtype=self.dtype), dims=self._dims, isherm=self._isherm, From fd5dbf0bbb6061959fe8780fa91d57347912a5fa Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Wed, 10 Jul 2024 18:58:27 +0530 Subject: [PATCH 280/305] fix indentation --- qutip/core/qobj.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index b75deb4883..b92d1d964b 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -824,9 +824,9 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: raise TypeError("expm is only valid for square operators") if isinstance(self.data, _data.CSR): return Qobj(_data.expm(self._data, dtype=dtype), - dims=self._dims, - isherm=self._isherm, - copy=False) + dims=self._dims, + isherm=self._isherm, + copy=False) return Qobj(_data.expm(self._data, dtype=self.dtype), dims=self._dims, isherm=self._isherm, From 9a0d2496021bfc12b0886e267358887fc7851e79 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 10 Jul 2024 16:55:35 -0400 Subject: [PATCH 281/305] Finish solver types --- qutip/core/coefficient.py | 1 + qutip/core/cy/coefficient.pyx | 2 +- qutip/solver/brmesolve.py | 40 +++++-- qutip/solver/floquet.py | 156 +++++++++++++++++++------- qutip/solver/mcsolve.py | 33 +++--- qutip/solver/mesolve.py | 15 ++- qutip/solver/multitraj.py | 4 +- qutip/solver/nm_mcsolve.py | 66 ++++++++--- qutip/solver/sesolve.py | 14 +-- qutip/solver/solver_base.py | 9 +- qutip/solver/stochastic.py | 141 ++++++++++++++++------- qutip/tests/solver/test_nm_mcsolve.py | 23 ++-- qutip/typing.py | 5 +- 13 files changed, 346 insertions(+), 163 deletions(-) diff --git a/qutip/core/coefficient.py b/qutip/core/coefficient.py index c1f259f455..3d5698de11 100644 --- a/qutip/core/coefficient.py +++ b/qutip/core/coefficient.py @@ -53,6 +53,7 @@ def _return(base, **kwargs): np.ndarray: InterCoefficient, scipy.interpolate.PPoly: InterCoefficient.from_PPoly, scipy.interpolate.BSpline: InterCoefficient.from_Bspline, + numbers.Number: ConstantCoefficient, } diff --git a/qutip/core/cy/coefficient.pyx b/qutip/core/cy/coefficient.pyx index a61ec16181..119a0fae00 100644 --- a/qutip/core/cy/coefficient.pyx +++ b/qutip/core/cy/coefficient.pyx @@ -745,7 +745,7 @@ cdef class ConstantCoefficient(Coefficient): """ cdef complex value - def __init__(self, complex value): + def __init__(self, complex value, **_): self.value = value def replace_arguments(self, _args=None, **kwargs): diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index e4c15d557c..e3c087beef 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -5,7 +5,9 @@ __all__ = ['brmesolve', 'BRSolver'] +from typing import Any import numpy as np +from numpy.typing import ArrayLike import inspect from time import time from .. import Qobj, QobjEvo, coefficient, Coefficient @@ -15,10 +17,21 @@ from .solver_base import Solver, _solver_deprecation from .options import _SolverOptions from ._feedback import _QobjFeedback, _DataFeedback - - -def brmesolve(H, psi0, tlist, a_ops=(), e_ops=(), c_ops=(), - args=None, sec_cutoff=0.1, options=None, **kwargs): +from ..typing import EopsLike, QobjEvoLike, CoefficientLike + + +def brmesolve( + H: QobjEvoLike, + psi0: Qobj, + tlist: ArrayLike, + a_ops: list[tuple[Qobj | QobjEvo, CoefficientLike]] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + c_ops: list[QobjEvoLike] = None, + args: dict[str, Any] = None, + sec_cutoff: float = 0.1, + options: dict[str, Any] = None, + **kwargs +): """ Solves for the dynamics of a system using the Bloch-Redfield master equation, given an input Hamiltonian, Hermitian bath-coupling terms and @@ -74,11 +87,11 @@ def brmesolve(H, psi0, tlist, a_ops=(), e_ops=(), c_ops=(), the operator: :obj:`.Qobj` vs :obj:`.QobjEvo` instead of the type of the spectra. - e_ops : list of :obj:`.Qobj` / callback function, optional - Single operator or list of operators for which to evaluate - expectation values or callable or list of callable. + e_ops : list, dict, :obj:`.Qobj` or callback function, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`expect` for more detail of operator expectation c_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format), optional List of collapse operators. @@ -147,6 +160,7 @@ def brmesolve(H, psi0, tlist, a_ops=(), e_ops=(), c_ops=(), c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] new_a_ops = [] + a_ops = a_ops if a_ops is not None else [] for (a_op, spectra) in a_ops: aop = QobjEvo(a_op, args=args, tlist=tlist) if isinstance(spectra, str): @@ -237,7 +251,15 @@ class BRSolver(Solver): } _avail_integrators = {} - def __init__(self, H, a_ops, c_ops=None, sec_cutoff=0.1, *, options=None): + def __init__( + self, + H: QobjEvoLike, + a_ops: list[tuple[Qobj | QobjEvo, Coefficient]], + c_ops: Qobj | QobjEvo | list[QobjEvoLike] = None, + sec_cutoff: float = 0.1, + *, + options: dict[str, Any] = None, + ): _time_start = time() self.rhs = None diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index 390029ae6e..d759ce5957 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -6,7 +6,9 @@ "FMESolver", ] +from typing import Any, overload, TypeVar, Literal, Callable import numpy as np +from numpy.typing import ArrayLike from qutip.core import data as _data from qutip.core.data import Data from qutip import Qobj, QobjEvo @@ -17,6 +19,7 @@ from .result import Result from time import time from ..ui.progressbar import progress_bars +from ..typing import EopsLike, QobjEvoLike class FloquetBasis: @@ -37,13 +40,13 @@ class FloquetBasis: def __init__( self, - H, - T, - args=None, - options=None, - sparse=False, - sort=True, - precompute=None, + H: QobjEvoLike, + T: float, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + sparse: bool = False, + sort: bool = True, + precompute: ArrayLike = None, ): """ Parameters @@ -120,7 +123,13 @@ def _as_ketlist(self, kets_mat): for ket in _data.split_columns(kets_mat) ] - def mode(self, t, data=False): + @overload + def mode(self, t: float, data: Literal[False]) -> Qobj: ... + + @overload + def mode(self, t: float, data: Literal[True]) -> Data: ... + + def mode(self, t: float, data: bool = False): """ Calculate the Floquet modes at time ``t``. @@ -151,7 +160,13 @@ def mode(self, t, data=False): else: return self._as_ketlist(kets_mat) - def state(self, t, data=False): + @overload + def state(self, t: float, data: Literal[False]) -> Qobj: ... + + @overload + def state(self, t: float, data: Literal[True]) -> Data: ... + + def state(self, t: float, data: bool = False): """ Evaluate the floquet states at time t. @@ -180,7 +195,13 @@ def state(self, t, data=False): else: return self._as_ketlist(states_mat) - def from_floquet_basis(self, floquet_basis, t=0): + QobjOrData = TypeVar("QobjOrData", Qobj, Data) + + def from_floquet_basis( + self, + floquet_basis: QobjOrData, + t: float = 0 + ) -> QobjOrData: """ Transform a ket or density matrix from the Floquet basis at time ``t`` to the lab basis. @@ -218,7 +239,11 @@ def from_floquet_basis(self, floquet_basis, t=0): return Qobj(lab_basis, dims=dims) return lab_basis - def to_floquet_basis(self, lab_basis, t=0): + def to_floquet_basis( + self, + lab_basis: QobjOrData, + t: float = 0 + ) -> QobjOrData: """ Transform a ket or density matrix in the lab basis to the Floquet basis at time ``t``. @@ -444,7 +469,15 @@ def _floquet_master_equation_tensor(A): return _data.add(R, S) -def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): +def floquet_tensor( + H: QobjEvo | FloquetBasis, + c_ops: list[Qobj], + spectra_cb: list[Callable[[float], complex]], + T: float = 0, + w_th: float = 0.0, + kmax: int = 5, + nT: int = 100, +) -> Qobj: """ Construct a tensor that represents the master equation in the floquet basis. @@ -456,10 +489,6 @@ def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): H : :obj:`.QobjEvo`, :obj:`.FloquetBasis` Periodic Hamiltonian a floquet basis system. - T : float, optional - The period of the time-dependence of the hamiltonian. Optional if ``H`` - is a ``FloquetBasis`` object. - c_ops : list of :class:`.Qobj` list of collapse operators. @@ -467,6 +496,10 @@ def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): List of callback functions that compute the noise power spectrum as a function of frequency for the collapse operators in `c_ops`. + T : float, optional + The period of the time-dependence of the hamiltonian. Optional if ``H`` + is a ``FloquetBasis`` object. + w_th : float, default: 0.0 The temperature in units of frequency. @@ -496,7 +529,15 @@ def floquet_tensor(H, c_ops, spectra_cb, T=0, w_th=0.0, kmax=5, nT=100): return Qobj(r, dims=[dims, dims], superrep="super", copy=False) -def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): +def fsesolve( + H: QobjEvoLike, + psi0: Qobj, + tlist: ArrayLike, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + T: float = 0.0, + args: dict[str, Any] = None, + options: dict[str, Any] = None, +) -> Result: """ Solve the Schrodinger equation using the Floquet formalism. @@ -513,10 +554,12 @@ def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): tlist : *list* / *array* List of times for :math:`t`. - e_ops : list of :class:`.Qobj` / callback function, optional - List of operators for which to evaluate expectation values. If this - list is empty, the state vectors for each time in `tlist` will be - returned instead of expectation values. + e_ops : list or dict of :class:`.Qobj` / callback function, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. + See :func:`~qutip.core.expect.expect` for more detail of operator + expectation. T : float, default=tlist[-1] The period of the time-dependence of the hamiltonian. @@ -569,17 +612,17 @@ def fsesolve(H, psi0, tlist, e_ops=None, T=0.0, args=None, options=None): def fmmesolve( - H, - rho0, - tlist, - c_ops=None, - e_ops=None, - spectra_cb=None, - T=0, - w_th=0.0, - args=None, - options=None, -): + H: QobjEvoLike, + rho0: Qobj, + tlist: ArrayLike, + c_ops: list[Qobj] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + spectra_cb: list[Callable[[float], complex]]= None, + T: float = 0.0, + w_th: float = 0.0, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + ) -> "FloquetResult": """ Solve the dynamics for the system using the Floquet-Markov master equation. @@ -601,8 +644,13 @@ def fmmesolve( supported. Fall back on :func:`fsesolve` if not provided. e_ops : list of :class:`.Qobj` / callback function, optional - List of operators for which to evaluate expectation values. - The states are reverted to the lab basis before applying the + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. + See :func:`~qutip.core.expect.expect` for more detail of operator + expectation. + The states are reverted to the lab basis before computing the + expectation values. spectra_cb : list callback functions, default: ``lambda w: (w > 0)`` List of callback functions that compute the noise power spectrum as @@ -773,7 +821,14 @@ class FMESolver(MESolver): } def __init__( - self, floquet_basis, a_ops, w_th=0.0, *, kmax=5, nT=None, options=None + self, + floquet_basis: FloquetBasis, + a_ops: list[tuple[Qobj, Callable[[float], float]]], + w_th: float = 0.0, + *, + kmax: int = 5, + nT: int = None, + options: dict[str, Any] = None, ): self.options = options if isinstance(floquet_basis, FloquetBasis): @@ -818,7 +873,7 @@ def _argument(self, args): if args: raise ValueError("FMESolver cannot update arguments") - def start(self, state0, t0, *, floquet=False): + def start(self, state0: Qobj, t0: float, *, floquet: bool=False) -> None: """ Set the initial state and time for a step evolution. ``options`` for the evolutions are read at this step. @@ -839,7 +894,14 @@ def start(self, state0, t0, *, floquet=False): state0 = self.floquet_basis.to_floquet_basis(state0, t0) super().start(state0, t0) - def step(self, t, *, args=None, copy=True, floquet=False): + def step( + self, + t: float, + *, + args: dict[str, Any] = None, + copy: bool = True, + floquet: bool = False, + ) -> Qobj: """ Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. @@ -873,7 +935,15 @@ def step(self, t, *, args=None, copy=True, floquet=False): state = state.copy() return state - def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): + def run( + self, + state0: Qobj, + tlist: ArrayLike, + *, + floquet: bool = False, + args: dict[str, Any] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + ) -> FloquetResult: """ Calculate the evolution of the quantum system. @@ -896,13 +966,13 @@ def run(self, state0, tlist, *, floquet=False, args=None, e_ops=None): floquet : bool, optional {False} Whether the initial state in the floquet basis or laboratory basis. - args : dict, optional {None} + args : dict, optional Not supported - e_ops : list {None} - List of Qobj, QobjEvo or callable to compute the expectation - values. Function[s] must have the signature - f(t : float, state : Qobj) -> expect. + e_ops : list or dict, optional + List or dict of Qobj, QobjEvo or callable to compute the + expectation values. Function[s] must have the signature + ``f(t : float, state : Qobj) -> expect``. Returns ------- diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 68f9b8caf8..364f4c6762 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -6,17 +6,18 @@ import numpy as np from numpy.typing import ArrayLike from numpy.random import SeedSequence +from time import time +from typing import Any, Callable +import warnings + from ..core import QobjEvo, spre, spost, Qobj, unstack_columns, qzero_like -from ..typing import QobjEvoLike +from ..typing import QobjEvoLike, EopsLike from .multitraj import MultiTrajSolver, _MultiTrajRHS, _InitialConditions from .solver_base import Solver, Integrator, _solver_deprecation from .multitrajresult import McResult from .mesolve import mesolve, MESolver from ._feedback import _QobjFeedback, _DataFeedback, _CollapseFeedback import qutip.core.data as _data -from time import time -from typing import Any, Callable -import warnings def mcsolve( @@ -24,13 +25,13 @@ def mcsolve( state: Qobj, tlist: ArrayLike, c_ops: QobjEvoLike | list[QobjEvoLike] = (), - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, ntraj: int = 500, *, args: dict[str, Any] = None, options: dict[str, Any] = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, - target_tol: float = None, + target_tol: float | tuple[float, float] | list[tuple[float, float]] = None, timeout: float = None, **kwargs, ) -> McResult: @@ -59,10 +60,10 @@ def mcsolve( even if ``H`` is a superoperator. If none are given, the solver will defer to ``sesolve`` or ``mesolve``. - e_ops : list, optional - A ``list`` of operator as Qobj, QobjEvo or callable with signature of - (t, state: Qobj) for calculating expectation values. When no ``e_ops`` - are given, the solver will default to save the states. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. ntraj : int, default: 500 Maximum number of trajectories to run. Can be cut short if a time limit @@ -562,8 +563,8 @@ def run( ntraj: int | list[int] = None, *, args: dict[str, Any] = None, - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, - target_tol: float = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + target_tol: float | tuple[float, float] | list[tuple[float, float]] = None, timeout: float = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> McResult: @@ -605,10 +606,10 @@ def run( args : dict, optional Change the ``args`` of the rhs for the evolution. - e_ops : list - list of Qobj or QobjEvo to compute the expectation values. - Alternatively, function[s] with the signature f(t, state) -> expect - can be used. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with + the signature `f(t: float, state: Qobj) -> Any`. timeout : float, optional Maximum time in seconds for the trajectories to run. Once this time diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index fac4de9321..686470b1e1 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -12,7 +12,7 @@ from typing import Any, Callable from time import time from .. import (Qobj, QobjEvo, liouvillian, lindblad_dissipator) -from ..typing import QobjEvoLike +from ..typing import EopsLike, QobjEvoLike from ..core import data as _data from .solver_base import Solver, _solver_deprecation from .sesolve import sesolve, SESolver @@ -24,8 +24,8 @@ def mesolve( H: QobjEvoLike, rho0: Qobj, tlist: ArrayLike, - c_ops: QobjEvoLike | list[QobjEvoLike] = None, - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + c_ops: Qobj | QobjEvo | list[QobjEvoLike] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, args: dict[str, Any] = None, options: dict[str, Any] = None, **kwargs @@ -87,11 +87,10 @@ def mesolve( Single collapse operator, or list of collapse operators, or a list of Liouvillian superoperators. None is equivalent to an empty list. - e_ops : list of :obj:`.Qobj` / callback function, optional - Single operator or list of operators for which to evaluate - expectation values or callable or list of callable. - Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`expect` for more detail of operator expectation. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. args : dict, optional dictionary of parameters for time-dependent Hamiltonians and diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index 646e38ab45..bbc74817ab 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -92,7 +92,7 @@ def __init__(self, rhs, *, options=None): self._state_metadata = {} self.stats = self._initialize_stats() - def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence = None): + def start(self, state0: Qobj, t0: float, seed: int | SeedSequence = None): """ Set the initial state and time for a step evolution. @@ -118,7 +118,7 @@ def start(self, state0: Qobj, t0: Number, seed: int | SeedSequence = None): self._integrator.set_state(t0, self._prepare_state(state0), generator) def step( - self, t: Number, *, args: dict[str, Any] = None, copy: bool = True + self, t: float, *, args: dict[str, Any] = None, copy: bool = True ) -> Qobj: """ Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index fb4677f835..a6ff5a16db 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -1,8 +1,11 @@ __all__ = ['nm_mcsolve', 'NonMarkovianMCSolver'] import numbers - +from typing import Any +from collections.abc import Sequence import numpy as np +from numpy.typing import ArrayLike +from numpy.random import SeedSequence import scipy from .multitraj import MultiTrajSolver @@ -10,10 +13,11 @@ from .mcsolve import MCSolver, MCIntegrator from .mesolve import MESolver, mesolve from .cy.nm_mcsolve import RateShiftCoefficient, SqrtRealCoefficient -from ..core.coefficient import ConstantCoefficient +from ..core.coefficient import ConstantCoefficient, Coefficient from ..core import ( CoreOptions, Qobj, QobjEvo, isket, ket2dm, qeye, coefficient, ) +from ..typing import QobjEvoLike, EopsLike, CoefficientLike # The algorithm implemented here is based on the influence martingale approach @@ -25,9 +29,21 @@ # https://arxiv.org/abs/2209.08958 -def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, - args=None, options=None, seeds=None, target_tol=None, - timeout=None): +def nm_mcsolve( + H: QobjEvoLike, + state: Qobj, + tlist: ArrayLike, + ops_and_rates: list[tuple[Qobj, float | CoefficientLike]] = (), + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + ntraj: int = 500, + *, + args: dict[str, Any] = None, + options: dict[str, Any] = None, + seeds: int | SeedSequence | list[int | SeedSequence] = None, + target_tol: float | tuple[float, float] | list[tuple[float, float]] = None, + timeout: float = None, + **kwargs, +) -> NmmcResult: """ Monte-Carlo evolution corresponding to a Lindblad equation with "rates" that may be negative. Usage of this function is analogous to ``mcsolve``, @@ -59,10 +75,10 @@ def nm_mcsolve(H, state, tlist, ops_and_rates=(), e_ops=None, ntraj=500, *, specified using any format accepted by :func:`~qutip.core.coefficient.coefficient`. - e_ops : list, optional - A ``list`` of operator as Qobj, QobjEvo or callable with signature of - (t, state: Qobj) for calculating expectation values. When no ``e_ops`` - are given, the solver will default to save the states. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. ntraj : int, default: 500 Maximum number of trajectories to run. Can be cut short if a time limit @@ -326,8 +342,8 @@ class NonMarkovianMCSolver(MCSolver): is a :class:`.Qobj` and ``Gamma`` represents the corresponding rate, which is allowed to be negative. The Lindblad operators must be operators even if ``H`` is a superoperator. Each rate ``Gamma`` may be - just a number (in the case of a constant rate) or, otherwise, specified - using any format accepted by :func:`qutip.coefficient`. + just a number (in the case of a constant rate) or, otherwise, a + :class:`~qutip.core.cy.Coefficient`. args : None / dict Arguments for time-dependent Hamiltonian and collapse operator terms. @@ -359,12 +375,16 @@ class NonMarkovianMCSolver(MCSolver): } def __init__( - self, H, ops_and_rates, args=None, options=None, + self, + H: Qobj | QobjEvo, + ops_and_rates = Sequence[tuple[Qobj, float | Coefficient]], + *, + options: dict[str, Any] = None, ): self.options = options ops_and_rates = [ - _parse_op_and_rate(op, rate, args=args or {}) + _parse_op_and_rate(op, rate) for op, rate in ops_and_rates ] a_parameter, L = self._check_completeness(ops_and_rates) @@ -519,12 +539,14 @@ def sqrt_shifted_rate(self, t, i): # in the step method. In the run-interface, the martingale is added as a # relative weight to the trajectory result at the end of `_run_one_traj`. - def start(self, state, t0, seed=None): + def start(self, state: Qobj, t0: float, seed: int | SeedSequence = None): self._martingale.initialize(t0, cache='clear') return super().start(state, t0, seed=seed) # The returned state will be a density matrix with trace=mu the martingale - def step(self, t, *, args=None, copy=True): + def step( + self, t: float, *, args: dict[str, Any] = None, copy: bool = True + ) -> Qobj: state = super().step(t, args=args, copy=copy) if isket(state): state = ket2dm(state) @@ -541,7 +563,15 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): result.trace = martingales return seed, result - def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): + def run( + self, + state: Qobj, + tlist: Sequence[float], + ntraj: int = 1, + *, + args: dict[str, Any] = None, + **kwargs + ): # update `args` dictionary before precomputing martingale self._argument(args) @@ -552,7 +582,7 @@ def run(self, state, tlist, ntraj=1, *, args=None, **kwargs): return result @property - def options(self): + def options(self) -> dict[str, Any]: """ Options for non-Markovian Monte Carlo solver: @@ -636,7 +666,7 @@ def options(self): return self._options @options.setter - def options(self, new_options): + def options(self, new_options: dict[str, Any]): MCSolver.options.fset(self, new_options) start.__doc__ = MultiTrajSolver.start.__doc__ diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 87c6f771c0..03d7d5dfc3 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -12,7 +12,7 @@ from typing import Any, Callable from .. import Qobj, QobjEvo from ..core import data as _data -from ..typing import QobjEvoLike +from ..typing import QobjEvoLike, EopsLike from .solver_base import Solver, _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback from . import Result @@ -22,7 +22,7 @@ def sesolve( H: QobjEvoLike, psi0: Qobj, tlist: ArrayLike, - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, args: dict[str, Any] = None, options: dict[str, Any] = None, **kwargs @@ -63,12 +63,10 @@ def sesolve( tlist : *list* / *array* list of times for :math:`t`. - e_ops : :obj:`.Qobj`, callable, or list, optional - Single operator or list of operators for which to evaluate - expectation values or callable or list of callable. - Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`~qutip.core.expect.expect` for more detail of operator - expectation. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. args : dict, optional dictionary of parameters for time-dependent Hamiltonians diff --git a/qutip/solver/solver_base.py b/qutip/solver/solver_base.py index eeee8981f6..34e62eba5e 100644 --- a/qutip/solver/solver_base.py +++ b/qutip/solver/solver_base.py @@ -14,6 +14,7 @@ from .integrator import Integrator from ..ui.progressbar import progress_bars from ._feedback import _ExpectFeedback +from ..typing import EopsLike from time import time import warnings import numpy as np @@ -142,7 +143,7 @@ def run( state0: Qobj, tlist: ArrayLike, *, - e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, args: dict[str, Any] = None, ) -> Result: """ @@ -167,9 +168,9 @@ def run( args : dict, optional Change the ``args`` of the rhs for the evolution. - e_ops : list, optional - List of Qobj, QobjEvo or callable to compute the expectation - values. Function[s] must have the signature + e_ops : Qobj, QobjEvo, callable, list, or dict optional + Single, list or dict of Qobj, QobjEvo or callable to compute the + expectation values. Function[s] must have the signature f(t : float, state : Qobj) -> expect. Returns diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 7606259c86..88f9c3ceda 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,5 +1,13 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] +import numpy as np +from numpy.typing import ArrayLike +from numpy.random import SeedSequence +from typing import Any, Callable +from functools import partial +from time import time +from collections.abc import Sequence + from .multitrajresult import MultiTrajResult from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .sode._noise import PreSetWiener @@ -7,11 +15,10 @@ from .multitraj import _MultiTrajRHS, MultiTrajSolver from .. import Qobj, QobjEvo from ..core.dimensions import Dimensions -import numpy as np -from functools import partial +from ..core import data as _data from .solver_base import _solver_deprecation from ._feedback import _QobjFeedback, _DataFeedback, _WienerFeedback -from time import time +from ..typing import QobjEvoLike, EopsLike class StochasticTrajResult(Result): @@ -35,7 +42,7 @@ def add(self, t, state, noise=None): self.noise.append(noise) @property - def wiener_process(self): + def wiener_process(self) -> np.typing.NDArray[float]: """ Wiener processes for each stochastic collapse operators. @@ -55,7 +62,7 @@ def wiener_process(self): return W @property - def dW(self): + def dW(self) -> np.typing.NDArray[float]: """ Wiener increment for each stochastic collapse operators. @@ -71,7 +78,7 @@ def dW(self): return noise @property - def measurement(self): + def measurement(self) -> np.typing.NDArray[float]: """ Measurements for each stochastic collapse operators. @@ -152,7 +159,7 @@ def _trajectories_attr(self, attr): return None @property - def measurement(self): + def measurement(self) -> np.typing.NDArray[float]: """ Measurements for each trajectories and stochastic collapse operators. @@ -165,7 +172,7 @@ def measurement(self): return self._trajectories_attr("measurement") @property - def dW(self): + def dW(self) -> np.typing.NDArray[float]: """ Wiener increment for each trajectories and stochastic collapse operators. @@ -179,7 +186,7 @@ def dW(self): return self._trajectories_attr("dW") @property - def wiener_process(self): + def wiener_process(self) -> np.typing.NDArray[float]: """ Wiener processes for each trajectories and stochastic collapse operators. @@ -192,7 +199,7 @@ def wiener_process(self): """ return self._trajectories_attr("wiener_process") - def merge(self, other, p=None): + def merge(self, other: "StochasticResult", p=None) -> "StochasticResult": if not isinstance(other, StochasticResult): return NotImplemented if self.stats["solver"] != other.stats["solver"]: @@ -299,10 +306,22 @@ def _register_feedback(self, val): def smesolve( - H, rho0, tlist, c_ops=(), sc_ops=(), heterodyne=False, *, - e_ops=(), args={}, ntraj=500, options=None, - seeds=None, target_tol=None, timeout=None, **kwargs -): + H: QobjEvoLike, + rho0: Qobj, + tlist: np.typing.ArrayLike, + c_ops: Qobj | QobjEvo | Sequence[QobjEvoLike] = (), + sc_ops: Qobj | QobjEvo | Sequence[QobjEvoLike] = (), + heterodyne: bool = False, + *, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + args: dict[str, Any] = None, + ntraj: int = 500, + options: dict[str, Any] = None, + seeds: int | SeedSequence | Sequence[int | SeedSequence] = None, + target_tol: float | tuple[float, float] | list[tuple[float, float]] = None, + timeout: float = None, + **kwargs +) -> StochasticResult: """ Solve stochastic master equation. @@ -326,11 +345,10 @@ def smesolve( sc_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) List of stochastic collapse operators. - e_ops : : :class:`.qobj`, callable, or list, optional - Single operator or list of operators for which to evaluate - expectation values or callable or list of callable. - Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`.expect` for more detail of operator expectation. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. args : dict, optional Dictionary of parameters for time-dependent Hamiltonians and @@ -418,6 +436,10 @@ def smesolve( """ options = _solver_deprecation(kwargs, options, "stoc") H = QobjEvo(H, args=args, tlist=tlist) + if not isinstance(sc_ops, Sequence): + sc_ops = [sc_ops] + if not isinstance(c_ops, Sequence): + c_ops = [c_ops] c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] sc_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in sc_ops] sol = SMESolver( @@ -430,10 +452,21 @@ def smesolve( def ssesolve( - H, psi0, tlist, sc_ops=(), heterodyne=False, *, - e_ops=(), args={}, ntraj=500, options=None, - seeds=None, target_tol=None, timeout=None, **kwargs -): + H: QobjEvoLike, + psi0: Qobj, + tlist: np.typing.ArrayLike, + sc_ops: QobjEvoLike | Sequence[QobjEvoLike] = (), + heterodyne: bool = False, + *, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + args: dict[str, Any] = None, + ntraj: int = 500, + options: dict[str, Any] = None, + seeds: int | SeedSequence | Sequence[int | SeedSequence] = None, + target_tol: float | tuple[float, float] | list[tuple[float, float]] = None, + timeout: float = None, + **kwargs +) -> StochasticResult: """ Solve stochastic Schrodinger equation. @@ -453,11 +486,10 @@ def ssesolve( sc_ops : list of (:obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format) List of stochastic collapse operators. - e_ops : :class:`.qobj`, callable, or list, optional - Single operator or list of operators for which to evaluate - expectation values or callable or list of callable. - Callable signature must be, `f(t: float, state: Qobj)`. - See :func:`expect` for more detail of operator expectation. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. args : dict, optional Dictionary of parameters for time-dependent Hamiltonians and @@ -543,6 +575,8 @@ def ssesolve( """ options = _solver_deprecation(kwargs, options, "stoc") H = QobjEvo(H, args=args, tlist=tlist) + if not isinstance(sc_ops, Sequence): + sc_ops = [sc_ops] sc_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in sc_ops] sol = SSESolver(H, sc_ops, options=options, heterodyne=heterodyne) return sol.run( @@ -601,7 +635,15 @@ def _initialize_stats(self): stats["solver"] = "Stochastic Schrodinger Equation Evolution" return stats - def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): + def __init__( + self, + H: Qobj | QobjEvo, + sc_ops: Sequence[Qobj | QobjEvo], + heterodyne: bool, + *, + c_ops: Sequence[Qobj | QobjEvo] = (), + options: dict[str, Any] = None, + ): self._heterodyne = heterodyne if self.name == "ssesolve" and c_ops: raise ValueError("c_ops are not supported by ssesolve.") @@ -619,15 +661,15 @@ def __init__(self, H, sc_ops, heterodyne, *, c_ops=(), options=None): self._dW_factors = np.ones(len(sc_ops)) @property - def heterodyne(self): + def heterodyne(self) -> bool: return self._heterodyne @property - def m_ops(self): + def m_ops(self) -> list[QobjEvo | Qobj]: return self._m_ops @m_ops.setter - def m_ops(self, new_m_ops): + def m_ops(self, new_m_ops: list[QobjEvo | Qobj]): """ Measurements operators. @@ -674,11 +716,11 @@ def m_ops(self, new_m_ops): self._m_ops = new_m_ops @property - def dW_factors(self): + def dW_factors(self) -> np.typing.NDArray[float]: return self._dW_factors @dW_factors.setter - def dW_factors(self, new_dW_factors): + def dW_factors(self, new_dW_factors: np.typing.NDArray[float]): """ Scaling of the noise on the measurements. Default are ``1`` for homodyne and ``sqrt(1/2)`` for heterodyne. @@ -702,8 +744,14 @@ def _integrate_one_traj(self, seed, tlist, result): return seed, result def run_from_experiment( - self, state, tlist, noise, *, - args=None, e_ops=(), measurement=False, + self, + state: Qobj, + tlist: np.typing.ArrayLike, + noise: Sequence[float], + *, + args: dict[str, Any] = None, + e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, + measurement: bool = False, ): """ Run a single trajectory from a given state and noise. @@ -728,8 +776,10 @@ def run_from_experiment( args : dict, optional Arguments to pass to the Hamiltonian and collapse operators. - e_ops : list, optional - List of operators for which to evaluate expectation values. + e_ops : :obj:`.Qobj`, callable, list or dict, optional + Single, list or dict of operators for which to evaluate + expectation values. Operator can be Qobj, QobjEvo or callables with the + signature `f(t: float, state: Qobj) -> Any`. measurement : bool, default : False Whether the passed noise is the Wiener increments ``dW`` (gaussian @@ -802,7 +852,7 @@ def avail_integrators(cls): } @property - def options(self): + def options(self) -> dict[str, Any]: """ Options for stochastic solver: @@ -863,11 +913,14 @@ def options(self): return self._options @options.setter - def options(self, new_options): + def options(self, new_options: dict[str, Any]): MultiTrajSolver.options.fset(self, new_options) @classmethod - def WienerFeedback(cls, default=None): + def WienerFeedback( + cls, + default: Callable[[float], np.typing.NDArray[float]] = None, + ): """ Wiener function of the trajectory argument for time dependent systems. @@ -896,7 +949,11 @@ def WienerFeedback(cls, default=None): return _WienerFeedback(default) @classmethod - def StateFeedback(cls, default=None, raw_data=False): + def StateFeedback( + cls, + default: Qobj | _data.Data = None, + raw_data: bool = False + ): """ State of the evolution to be used in a time-dependent operator. diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index 091bd7383b..7fc0fababe 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -110,12 +110,13 @@ def test_solver_pickleable(): "sin(t)", ] args = [ - None, + {}, {'constant': 1}, - None, + {}, ] for rate, arg in zip(rates, args): - solver = NonMarkovianMCSolver(H, [(L, rate)], args=arg) + op_and_rate = (L, qutip.coefficient(rate, args=arg)) + solver = NonMarkovianMCSolver(H, [op_and_rate]) jar = pickle.dumps(solver) loaded_solver = pickle.loads(jar) @@ -559,9 +560,9 @@ def test_stepping(self): size = 10 a = qutip.destroy(size) H = qutip.num(size) - ops_and_rates = [(a, 'alpha')] + ops_and_rates = [(a, qutip.coefficient('alpha', args={'alpha': 0}))] mcsolver = NonMarkovianMCSolver( - H, ops_and_rates, args={'alpha': 0}, options={'map': 'serial'}, + H, ops_and_rates, options={'map': 'serial'}, ) mcsolver.start(qutip.basis(size, size-1), 0, seed=5) state_1 = mcsolver.step(1, args={'alpha': 1}) @@ -630,12 +631,12 @@ def test_super_H(improved_sampling, mixed_initial_state): def test_NonMarkovianMCSolver_run(): size = 10 + args = {'coupling': 0} ops_and_rates = [ - (qutip.destroy(size), 'coupling') + (qutip.destroy(size), qutip.coefficient('coupling', args=args)) ] - args = {'coupling': 0} H = qutip.num(size) - solver = NonMarkovianMCSolver(H, ops_and_rates, args=args) + solver = NonMarkovianMCSolver(H, ops_and_rates) solver.options = {'store_final_state': True} res = solver.run(qutip.basis(size, size-1), np.linspace(0, 5.0, 11), e_ops=[qutip.qeye(size)], args={'coupling': 1}) @@ -653,12 +654,12 @@ def test_NonMarkovianMCSolver_run(): def test_NonMarkovianMCSolver_stepping(): size = 10 + args = {'coupling': 0} ops_and_rates = [ - (qutip.destroy(size), 'coupling') + (qutip.destroy(size), qutip.coefficient('coupling', args=args)) ] - args = {'coupling': 0} H = qutip.num(size) - solver = NonMarkovianMCSolver(H, ops_and_rates, args=args) + solver = NonMarkovianMCSolver(H, ops_and_rates) solver.start(qutip.basis(size, size-1), 0, seed=0) state = solver.step(1) assert qutip.expect(qutip.qeye(size), state) == pytest.approx(1) diff --git a/qutip/typing.py b/qutip/typing.py index cbc4aa066c..30d2918ca4 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -1,4 +1,4 @@ -from typing import Sequence, Union, Any, Protocol +from typing import Sequence, Union, Any, Protocol, Callable from numbers import Number, Real import numpy as np import scipy.interpolate @@ -28,6 +28,9 @@ def __call__(self, t: Real, **kwargs) -> Number: ] +EopsLike = Union["Qobj", "QobjEvo", Callable[[float, "Qobj"], Any]] + + ElementType = Union[QEvoProtocol, "Qobj", tuple["Qobj", CoefficientLike]] From f23f92c6c20a7b2dffb965855365340504f5700c Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Thu, 11 Jul 2024 13:48:26 -0400 Subject: [PATCH 282/305] Remove args param to Solver --- doc/conf.py | 1 + qutip/solver/nm_mcsolve.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 7fc7ef7fa7..57c6b09c68 100755 --- a/doc/conf.py +++ b/doc/conf.py @@ -368,6 +368,7 @@ def qutip_version(): 'CoefficientLike': 'CoefficientLike', 'ElementType': 'ElementType', 'QobjEvoLike': 'QobjEvoLike', + 'EopsLike': 'EopsLike', 'LayerType': 'LayerType', 'ArrayLike': 'ArrayLike' } diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index a6ff5a16db..771f5f8bed 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -345,9 +345,6 @@ class NonMarkovianMCSolver(MCSolver): just a number (in the case of a constant rate) or, otherwise, a :class:`~qutip.core.cy.Coefficient`. - args : None / dict - Arguments for time-dependent Hamiltonian and collapse operator terms. - options : SolverOptions, [optional] Options for the evolution. """ From 0f15387c9b0634ee6853ddb5f4bc54406197e467 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Fri, 12 Jul 2024 11:50:58 +0530 Subject: [PATCH 283/305] check for dia --- qutip/core/qobj.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index b92d1d964b..fb33df6071 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -798,7 +798,7 @@ def diag(self) -> np.ndarray: out = np.real(out) return out - def expm(self, dtype: LayerType = _data.Dense) -> Qobj: + def expm(self, dtype) -> Qobj: """Matrix exponential of quantum operator. Input operator must be square. @@ -822,12 +822,13 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """ if not self._dims.issquare: raise TypeError("expm is only valid for square operators") - if isinstance(self.data, _data.CSR): - return Qobj(_data.expm(self._data, dtype=dtype), + if isinstance(self.data, _data.CSR) or isinstance( + self.data, _data.Dia): + return Qobj(_data.expm(self._data, dtype=_data.Dense), dims=self._dims, isherm=self._isherm, copy=False) - return Qobj(_data.expm(self._data, dtype=self.dtype), + return Qobj(_data.expm(self._data, dtype=dtype), dims=self._dims, isherm=self._isherm, copy=False) From f029e558d1681b513ff7722178454dfe2ff9cb8a Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Fri, 12 Jul 2024 11:54:08 +0530 Subject: [PATCH 284/305] corrections --- qutip/core/qobj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index fb33df6071..2f5aa0a345 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -828,7 +828,7 @@ def expm(self, dtype) -> Qobj: dims=self._dims, isherm=self._isherm, copy=False) - return Qobj(_data.expm(self._data, dtype=dtype), + return Qobj(_data.expm(self._data, dtype=self.dtype), dims=self._dims, isherm=self._isherm, copy=False) From c3f5dc688b9090d68d907835514b6e0fa6d96e57 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Fri, 12 Jul 2024 12:19:55 +0530 Subject: [PATCH 285/305] add default --- qutip/core/qobj.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 2f5aa0a345..cb1b1f3061 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -798,7 +798,7 @@ def diag(self) -> np.ndarray: out = np.real(out) return out - def expm(self, dtype) -> Qobj: + def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """Matrix exponential of quantum operator. Input operator must be square. From 06efde0074365f815695101e12ddee52ebe9b171 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 15 Jul 2024 10:14:57 -0400 Subject: [PATCH 286/305] Make QobjOrData a common type --- qutip/solver/floquet.py | 4 +--- qutip/typing.py | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index d759ce5957..cf8c30cb92 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -19,7 +19,7 @@ from .result import Result from time import time from ..ui.progressbar import progress_bars -from ..typing import EopsLike, QobjEvoLike +from ..typing import EopsLike, QobjEvoLike, QobjOrData class FloquetBasis: @@ -195,8 +195,6 @@ def state(self, t: float, data: bool = False): else: return self._as_ketlist(states_mat) - QobjOrData = TypeVar("QobjOrData", Qobj, Data) - def from_floquet_basis( self, floquet_basis: QobjOrData, diff --git a/qutip/typing.py b/qutip/typing.py index 30d2918ca4..ec2293b5f5 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -1,4 +1,4 @@ -from typing import Sequence, Union, Any, Protocol, Callable +from typing import Sequence, Union, Any, Protocol, Callable, TypeVar from numbers import Number, Real import numpy as np import scipy.interpolate @@ -28,6 +28,9 @@ def __call__(self, t: Real, **kwargs) -> Number: ] +QobjOrData = TypeVar("QobjOrData", "Qobj", "Data") + + EopsLike = Union["Qobj", "QobjEvo", Callable[[float, "Qobj"], Any]] From ed4b7e90b356b89746cbda011334c111eaac2d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Mon, 15 Jul 2024 11:35:01 -0400 Subject: [PATCH 287/305] Update qutip/core/expect.py Co-authored-by: Simon Cross --- qutip/core/expect.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/core/expect.py b/qutip/core/expect.py index d4a5447e2f..647aba3c69 100644 --- a/qutip/core/expect.py +++ b/qutip/core/expect.py @@ -48,7 +48,7 @@ def expect(oper, state): ------- expt : float / complex / list / array Expectation value(s). ``real`` if ``oper`` is Hermitian, ``complex`` - otherwise. If multiple ``oper`` are passed, a list of array + otherwise. If multiple ``oper`` are passed, a list of array. A (nested) array of expectaction values if ``state`` or ``oper`` are arrays. From bfe7cf651c7d95ef2c069cd33b875fea9f501d73 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 15 Jul 2024 15:00:04 -0400 Subject: [PATCH 288/305] Fix step for stochastic solver --- qutip/solver/stochastic.py | 17 +++++++++++++++++ qutip/tests/solver/test_stochastic.py | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 7606259c86..c39ceecb91 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,5 +1,6 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] +from typing import Any from .multitrajresult import MultiTrajResult from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .sode._noise import PreSetWiener @@ -792,6 +793,22 @@ def run_from_experiment( result.stats.update(stats) return result + def step( + self, t: float, + *, + args: dict[str, Any] = None, + copy: bool = True, + wiener_increment = False, + ) -> Qobj: + if not self._integrator._is_set: + raise RuntimeError("The `start` method must called first.") + self._argument(args) + _, state, dW = self._integrator.integrate(t, copy=False) + state = self._restore_state(state, copy=copy) + if wiener_increment: + return state, dW + return state + @classmethod def avail_integrators(cls): if cls is StochasticSolver: diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index cf63bef666..270052ba8b 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -562,3 +562,25 @@ def test_merge_results(store_measurement, keep_runs_results): w.shape == result_merged.wiener_process[0].shape for w in result_merged.wiener_process ) + +@pytest.mark.parametrize("open", [True, False]) +def test_step(open): + state0 = basis(5, 3) + kw = {} + if open: + SolverCls = SMESolver + state0 = state0.proj() + else: + SolverCls = SSESolver + + solver = SolverCls( + num(5), + sc_ops=[destroy(5)], + heterodyne=False, + options={"dt": 0.001}, + **kw + ) + solver.start(state0, t0=0) + state1 = solver.step(0.01) + assert state1.dims == state0.dims + assert state1.norm() == pytest.approx(1, abs=0.001) From 04051ae2e89065df622b195f61125e009ed71fa9 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 15 Jul 2024 15:02:25 -0400 Subject: [PATCH 289/305] Add tests with wiener increment output --- qutip/tests/solver/test_stochastic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 270052ba8b..f3f9eae5fa 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -584,3 +584,7 @@ def test_step(open): state1 = solver.step(0.01) assert state1.dims == state0.dims assert state1.norm() == pytest.approx(1, abs=0.001) + state2, dW = solver.step(0.02, wiener_increment=True) + assert state2.dims == state0.dims + assert state2.norm() == pytest.approx(1, abs=0.001) + assert abs(dW) < 0.5 # 5 sigmas From 906bd37a7edb8063778d975434b56914689f82ae Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 15 Jul 2024 15:15:48 -0400 Subject: [PATCH 290/305] Fix step for stochastic solver --- qutip/solver/stochastic.py | 43 +++++++++++++++++++++++---- qutip/tests/solver/test_stochastic.py | 6 ++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index c39ceecb91..5b52cd53e7 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -1,6 +1,6 @@ __all__ = ["smesolve", "SMESolver", "ssesolve", "SSESolver"] -from typing import Any +from typing import Any, Literal, overload from .multitrajresult import MultiTrajResult from .sode.ssystem import StochasticOpenSystem, StochasticClosedSystem from .sode._noise import PreSetWiener @@ -793,19 +793,52 @@ def run_from_experiment( result.stats.update(stats) return result + @overload def step( self, t: float, *, - args: dict[str, Any] = None, - copy: bool = True, - wiener_increment = False, - ) -> Qobj: + args: dict[str, Any], + copy: bool, + wiener_increment: Literal[False], + ) -> Qobj: ... + + @overload + def step( + self, t: float, + *, + args: dict[str, Any], + copy: bool, + wiener_increment: Literal[True], + ) -> tuple[Qobj, np.typing.NDArray[float]]: ... + + def step(self, t, *, args=None, copy=True, wiener_increment=False): + """ + Evolve the state to ``t`` and return the state as a :obj:`.Qobj`. + + Parameters + ---------- + t : double + Time to evolve to, must be higher than the last call. + + args : dict, optional + Update the ``args`` of the system. + The change is effective from the beginning of the interval. + Changing ``args`` can slow the evolution. + + copy : bool, default: True + Whether to return a copy of the data or the data in the ODE solver. + + wiener_increment: bool, default: False + Whether to return ``dW`` with for the step with the state. + """ if not self._integrator._is_set: raise RuntimeError("The `start` method must called first.") self._argument(args) _, state, dW = self._integrator.integrate(t, copy=False) state = self._restore_state(state, copy=copy) if wiener_increment: + if self.heterodyne: + dW = dW.reshape(-1, 2) return state, dW return state diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index f3f9eae5fa..5ef974a77c 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -575,8 +575,8 @@ def test_step(open): solver = SolverCls( num(5), - sc_ops=[destroy(5)], - heterodyne=False, + sc_ops=[destroy(5), destroy(5)**2 / 10], + heterodyne=True, options={"dt": 0.001}, **kw ) @@ -587,4 +587,4 @@ def test_step(open): state2, dW = solver.step(0.02, wiener_increment=True) assert state2.dims == state0.dims assert state2.norm() == pytest.approx(1, abs=0.001) - assert abs(dW) < 0.5 # 5 sigmas + assert abs(dW[0, 0]) < 0.5 # 5 sigmas From 549757efe089396b151dd0e04e0834072610538b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 15 Jul 2024 17:44:43 -0400 Subject: [PATCH 291/305] Add heterodyne tests --- qutip/tests/solver/test_stochastic.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/qutip/tests/solver/test_stochastic.py b/qutip/tests/solver/test_stochastic.py index 5ef974a77c..dc375c705b 100644 --- a/qutip/tests/solver/test_stochastic.py +++ b/qutip/tests/solver/test_stochastic.py @@ -564,7 +564,8 @@ def test_merge_results(store_measurement, keep_runs_results): ) @pytest.mark.parametrize("open", [True, False]) -def test_step(open): +@pytest.mark.parametrize("heterodyne", [True, False]) +def test_step(open, heterodyne): state0 = basis(5, 3) kw = {} if open: @@ -576,15 +577,20 @@ def test_step(open): solver = SolverCls( num(5), sc_ops=[destroy(5), destroy(5)**2 / 10], - heterodyne=True, + heterodyne=heterodyne, options={"dt": 0.001}, **kw ) solver.start(state0, t0=0) state1 = solver.step(0.01) assert state1.dims == state0.dims - assert state1.norm() == pytest.approx(1, abs=0.001) + assert state1.norm() == pytest.approx(1, abs=0.01) state2, dW = solver.step(0.02, wiener_increment=True) assert state2.dims == state0.dims - assert state2.norm() == pytest.approx(1, abs=0.001) - assert abs(dW[0, 0]) < 0.5 # 5 sigmas + assert state2.norm() == pytest.approx(1, abs=0.01) + if heterodyne: + assert dW.shape == (2, 2) + assert abs(dW[0, 0]) < 0.5 # 5 sigmas + else: + assert dW.shape == (2,) + assert abs(dW[0]) < 0.5 # 5 sigmas From ee47a8284142f156a06b483a0db553b3b37db26f Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 15 Jul 2024 17:45:03 -0400 Subject: [PATCH 292/305] Add towncrier --- doc/changes/2491.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/2491.bugfix diff --git a/doc/changes/2491.bugfix b/doc/changes/2491.bugfix new file mode 100644 index 0000000000..2abe5945d4 --- /dev/null +++ b/doc/changes/2491.bugfix @@ -0,0 +1 @@ +Fix stochastic solver step method From 3962fe97ed60c79355d19d3c29f8943b0d975712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Mon, 15 Jul 2024 17:45:52 -0400 Subject: [PATCH 293/305] Update qutip/solver/stochastic.py Co-authored-by: Simon Cross --- qutip/solver/stochastic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 5b52cd53e7..b8c0d08f6e 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -829,7 +829,7 @@ def step(self, t, *, args=None, copy=True, wiener_increment=False): Whether to return a copy of the data or the data in the ODE solver. wiener_increment: bool, default: False - Whether to return ``dW`` with for the step with the state. + Whether to return ``dW`` in addition to the state. """ if not self._integrator._is_set: raise RuntimeError("The `start` method must called first.") From 8210e1fe3bd661af9db27efa003551e7b0c7dc11 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Tue, 16 Jul 2024 10:43:50 +0530 Subject: [PATCH 294/305] cleaning the code --- qutip/core/qobj.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index cb1b1f3061..6db7ca52d9 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -822,13 +822,11 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """ if not self._dims.issquare: raise TypeError("expm is only valid for square operators") - if isinstance(self.data, _data.CSR) or isinstance( - self.data, _data.Dia): - return Qobj(_data.expm(self._data, dtype=_data.Dense), - dims=self._dims, - isherm=self._isherm, - copy=False) - return Qobj(_data.expm(self._data, dtype=self.dtype), + if isinstance(self.data, (_data.CSR, _data.Dia)): + dtype = self.dtype + else: + dtype = None + return Qobj(_data.expm(self._data, dtype=dtype), dims=self._dims, isherm=self._isherm, copy=False) From e25e006b1f1c19d927ef4dbadc71b4993a499921 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Tue, 16 Jul 2024 12:58:02 +0530 Subject: [PATCH 295/305] cleaning the code-2 --- qutip/core/qobj.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 6db7ca52d9..6797215661 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -822,9 +822,7 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """ if not self._dims.issquare: raise TypeError("expm is only valid for square operators") - if isinstance(self.data, (_data.CSR, _data.Dia)): - dtype = self.dtype - else: + if not isinstance(self.data, (_data.CSR, _data.Dia)): dtype = None return Qobj(_data.expm(self._data, dtype=dtype), dims=self._dims, From 9af76cb3555c92ec5058eb668fed7a8853c2a07b Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Tue, 16 Jul 2024 08:53:30 -0400 Subject: [PATCH 296/305] Fix mcsolve type hint --- qutip/solver/brmesolve.py | 2 +- qutip/solver/mcsolve.py | 4 ++-- qutip/solver/nm_mcsolve.py | 30 ++++++++++++++++++--------- qutip/tests/solver/test_nm_mcsolve.py | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index e3c087beef..507bb21e31 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -253,7 +253,7 @@ class BRSolver(Solver): def __init__( self, - H: QobjEvoLike, + H: Qobj | QobjEvo, a_ops: list[tuple[Qobj | QobjEvo, Coefficient]], c_ops: Qobj | QobjEvo | list[QobjEvoLike] = None, sec_cutoff: float = 0.1, diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index 364f4c6762..b6cbd2baf0 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -454,8 +454,8 @@ class MCSolver(MultiTrajSolver): def __init__( self, - H: QobjEvoLike, - c_ops: QobjEvoLike | list[QobjEvoLike], + H: Qobj | QobjEvo, + c_ops: Qobj | QobjEvo | list[Qobj | QobjEvo], *, options: dict[str, Any] = None, ): diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 771f5f8bed..7615bb4d0d 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -380,22 +380,32 @@ def __init__( ): self.options = options - ops_and_rates = [ - _parse_op_and_rate(op, rate) - for op, rate in ops_and_rates - ] - a_parameter, L = self._check_completeness(ops_and_rates) + self.ops = [] + self._rates = [] + + for op, rate in ops_and_rates: + if not isinstance(op, Qobj): + raise ValueError("ops_and_rates' ops must be Qobj") + if isinstance(rate, numbers.Number): + rate = ConstantCoefficient(rate) + if not isinstance(rate, Coefficient): + raise ValueError( + "ops_and_rates' rates must be scalar or Coefficient" + ) + self.ops.append(op) + self._rates.append(rate) + + a_parameter, L = self._check_completeness(self.ops) if L is not None: - ops_and_rates.append((L, ConstantCoefficient(0))) + self.ops.append(L) + self._rates.append(ConstantCoefficient(0)) - self.ops = [op for op, _ in ops_and_rates] self._martingale = InfluenceMartingale( self, a_parameter, self.options["martingale_quad_limit"] ) # Many coefficients. These should not be publicly exposed # and will all need to be updated in _arguments(): - self._rates = [rate for _, rate in ops_and_rates] self._rate_shift = RateShiftCoefficient(self._rates) self._sqrt_shifted_rates = [ SqrtRealCoefficient(rate + self._rate_shift) @@ -412,7 +422,7 @@ def __init__( def _mc_integrator_class(self, *args): return NmMCIntegrator(*args, __martingale=self._martingale) - def _check_completeness(self, ops_and_rates): + def _check_completeness(self, ops): """ Checks whether ``sum(Li.dag() * Li)`` is proportional to the identity operator. If not, creates an extra Lindblad operator so that it is. @@ -420,7 +430,7 @@ def _check_completeness(self, ops_and_rates): Returns the proportionality factor a, and the extra Lindblad operator (or None if no extra Lindblad operator is necessary). """ - op = sum((L.dag() * L) for L, _ in ops_and_rates) + op = sum((L.dag() * L) for L in ops) a_candidate = op.tr() / op.shape[0] with CoreOptions(rtol=self.options["completeness_rtol"], diff --git a/qutip/tests/solver/test_nm_mcsolve.py b/qutip/tests/solver/test_nm_mcsolve.py index 7fc0fababe..bde9cad57f 100644 --- a/qutip/tests/solver/test_nm_mcsolve.py +++ b/qutip/tests/solver/test_nm_mcsolve.py @@ -756,7 +756,7 @@ def rate_function(t): ntraj = [3, 9] solver = qutip.NonMarkovianMCSolver( - H, [(L, rate_function)], + H, [(L, qutip.coefficient(rate_function))], options={'improved_sampling': improved_sampling}) mixed_result = solver.run( [(initial_state1, p), (initial_state2, 1 - p)], tlist, ntraj) From 021abffdef7411603b6b66b392ef66215b6ae99b Mon Sep 17 00:00:00 2001 From: Sampreet Kalita <9553215+Sampreet@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:47:23 +0530 Subject: [PATCH 297/305] Support measurement for JAX --- doc/changes/2493.feature | 1 + qutip/measurement.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 doc/changes/2493.feature diff --git a/doc/changes/2493.feature b/doc/changes/2493.feature new file mode 100644 index 0000000000..b7bb95f78e --- /dev/null +++ b/doc/changes/2493.feature @@ -0,0 +1 @@ +Support measurement statistics for `jax` and `jaxdia` dtypes \ No newline at end of file diff --git a/qutip/measurement.py b/qutip/measurement.py index 5b29c40836..570c55bc2e 100644 --- a/qutip/measurement.py +++ b/qutip/measurement.py @@ -239,7 +239,7 @@ def measurement_statistics_observable(state, op, tol=None): if probability >= tol: probabilities.append(probability) - values.append(np.mean(eigenvalues[present_group])) + values.append(np.mean(eigenvalues[np.array(present_group)])) projectors.append(projector) present_group = [] From e4f8fbb59d7a0c9c58a4b6209118df8778c348a7 Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Wed, 17 Jul 2024 17:53:46 +0530 Subject: [PATCH 298/305] change function definition --- qutip/core/qobj.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py index 6797215661..c82a0d6db4 100644 --- a/qutip/core/qobj.py +++ b/qutip/core/qobj.py @@ -798,7 +798,7 @@ def diag(self) -> np.ndarray: out = np.real(out) return out - def expm(self, dtype: LayerType = _data.Dense) -> Qobj: + def expm(self, dtype: LayerType = None) -> Qobj: """Matrix exponential of quantum operator. Input operator must be square. @@ -806,9 +806,7 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: Parameters ---------- dtype : type - The data-layer type that should be output. As the matrix - exponential is almost dense, this defaults to outputting dense - matrices. + The data-layer type that should be output. Returns ------- @@ -822,8 +820,8 @@ def expm(self, dtype: LayerType = _data.Dense) -> Qobj: """ if not self._dims.issquare: raise TypeError("expm is only valid for square operators") - if not isinstance(self.data, (_data.CSR, _data.Dia)): - dtype = None + if dtype is None and isinstance(self.data, (_data.CSR, _data.Dia)): + dtype = _data.Dense return Qobj(_data.expm(self._data, dtype=dtype), dims=self._dims, isherm=self._isherm, From aec3b68b59c41c2cca3b20db7feff37ea975d93f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Gigu=C3=A8re?= Date: Wed, 17 Jul 2024 15:19:23 -0400 Subject: [PATCH 299/305] Apply suggestions from code review Co-authored-by: Paul --- qutip/solver/brmesolve.py | 6 +++--- qutip/solver/mcsolve.py | 2 +- qutip/solver/stochastic.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index 507bb21e31..705f96ca95 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -24,7 +24,7 @@ def brmesolve( H: QobjEvoLike, psi0: Qobj, tlist: ArrayLike, - a_ops: list[tuple[Qobj | QobjEvo, CoefficientLike]] = None, + a_ops: list[tuple[QobjEvoLike, CoefficientLike]] = None, e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, c_ops: list[QobjEvoLike] = None, args: dict[str, Any] = None, @@ -160,7 +160,7 @@ def brmesolve( c_ops = [QobjEvo(c_op, args=args, tlist=tlist) for c_op in c_ops] new_a_ops = [] - a_ops = a_ops if a_ops is not None else [] + a_ops = a_ops or [] for (a_op, spectra) in a_ops: aop = QobjEvo(a_op, args=args, tlist=tlist) if isinstance(spectra, str): @@ -255,7 +255,7 @@ def __init__( self, H: Qobj | QobjEvo, a_ops: list[tuple[Qobj | QobjEvo, Coefficient]], - c_ops: Qobj | QobjEvo | list[QobjEvoLike] = None, + c_ops: Qobj | QobjEvo | list[Qobj | QobjEvo] = None, sec_cutoff: float = 0.1, *, options: dict[str, Any] = None, diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index b6cbd2baf0..3623d14c4b 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -7,7 +7,7 @@ from numpy.typing import ArrayLike from numpy.random import SeedSequence from time import time -from typing import Any, Callable +from typing import Any import warnings from ..core import QobjEvo, spre, spost, Qobj, unstack_columns, qzero_like diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 88f9c3ceda..6f11c16bda 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -199,7 +199,7 @@ def wiener_process(self) -> np.typing.NDArray[float]: """ return self._trajectories_attr("wiener_process") - def merge(self, other: "StochasticResult", p=None) -> "StochasticResult": + def merge(self, other: "StochasticResult", p: float = None) -> "StochasticResult": if not isinstance(other, StochasticResult): return NotImplemented if self.stats["solver"] != other.stats["solver"]: From 33da4864b3e5ed3f3d63e6d283e1ce37c4997637 Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 17 Jul 2024 15:20:10 -0400 Subject: [PATCH 300/305] Fix issues from review --- qutip/solver/brmesolve.py | 6 +++--- qutip/solver/floquet.py | 22 +++++++++++++--------- qutip/solver/mcsolve.py | 8 ++++---- qutip/solver/mesolve.py | 2 +- qutip/solver/multitraj.py | 2 +- qutip/solver/nm_mcsolve.py | 2 +- qutip/solver/sesolve.py | 2 +- qutip/solver/stochastic.py | 16 ++++++++-------- 8 files changed, 32 insertions(+), 28 deletions(-) diff --git a/qutip/solver/brmesolve.py b/qutip/solver/brmesolve.py index 507bb21e31..0d48a1bbad 100644 --- a/qutip/solver/brmesolve.py +++ b/qutip/solver/brmesolve.py @@ -55,7 +55,7 @@ def brmesolve( Nested list of system operators that couple to the environment, and the corresponding bath spectra. - a_op : :obj:`.Qobj`, :obj:`.QobjEvo` + a_op : :obj:`.Qobj`, :obj:`.QobjEvo`, :obj:`.QobjEvo` compatible format The operator coupling to the environment. Must be hermitian. spectra : :obj:`.Coefficient`, str, func @@ -75,7 +75,7 @@ def brmesolve( a_ops = [ (a+a.dag(), ('w>0', args={"w": 0})), (QobjEvo(a+a.dag()), 'w > exp(-t)'), - (QobjEvo([b+b.dag(), lambda t: ...]), lambda w: ...)), + ([[b+b.dag(), lambda t: ...]], lambda w: ...)), (c+c.dag(), SpectraCoefficient(coefficient(array, tlist=ws))), ] @@ -88,7 +88,7 @@ def brmesolve( of the spectra. e_ops : list, dict, :obj:`.Qobj` or callback function, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. Callable signature must be, `f(t: float, state: Qobj)`. diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index cf8c30cb92..02921ac594 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -47,6 +47,7 @@ def __init__( sparse: bool = False, sort: bool = True, precompute: ArrayLike = None, + tlist: ArrayLike = None, ): """ Parameters @@ -76,10 +77,14 @@ def __init__( for later use when computing modes and states. Default is ``linspace(0, T, 101)`` corresponding to the default integration steps used for the floquet tensor computation. + + times : ArrayLike [None] + Time for array """ if not T > 0: raise ValueError("The period need to be a positive number.") self.T = T + H = QobjEvo(H, args=args, tlist=times) if precompute is not None: tlist = np.unique(np.atleast_1d(precompute) % self.T) memoize = len(tlist) @@ -91,12 +96,9 @@ def __init__( # Default computation tlist = np.linspace(0, T, 101) memoize = 101 - if ( - isinstance(H, QobjEvo) - and (H._feedback_functions or H._solver_only_feedback) - ): + if H._feedback_functions or H._solver_only_feedback: raise NotImplementedError("FloquetBasis does not support feedback") - self.U = Propagator(H, args=args, options=options, memoize=memoize) + self.U = Propagator(H, options=options, memoize=memoize) for t in tlist: # Do the evolution by steps to save the intermediate results. self.U(t) @@ -553,7 +555,7 @@ def fsesolve( List of times for :math:`t`. e_ops : list or dict of :class:`.Qobj` / callback function, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. See :func:`~qutip.core.expect.expect` for more detail of operator @@ -592,7 +594,8 @@ def fsesolve( T = T or tlist[-1] # `fsesolve` is a fallback from `fmmesolve`, for the later, options # are for the open system evolution. - floquet_basis = FloquetBasis(H, T, args, precompute=tlist) + H = QobjEvo(H, args=args, tlist=tlist, copy=False) + floquet_basis = FloquetBasis(H, T, precompute=tlist) f_coeff = floquet_basis.to_floquet_basis(psi0) result_options = { @@ -642,7 +645,7 @@ def fmmesolve( supported. Fall back on :func:`fsesolve` if not provided. e_ops : list of :class:`.Qobj` / callback function, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. See :func:`~qutip.core.expect.expect` for more detail of operator @@ -733,7 +736,8 @@ def fmmesolve( t_precompute = np.concatenate([tlist, np.linspace(0, T, 101)]) # `fsesolve` is a fallback from `fmmesolve`, for the later, options # are for the open system evolution. - floquet_basis = FloquetBasis(H, T, args, precompute=t_precompute) + H = QobjEvo(H, args=args, tlist=tlist, copy=False) + floquet_basis = FloquetBasis(H, T, precompute=t_precompute) if not w_th and args: w_th = args.get("w_th", 0.0) diff --git a/qutip/solver/mcsolve.py b/qutip/solver/mcsolve.py index b6cbd2baf0..28ea219bbc 100644 --- a/qutip/solver/mcsolve.py +++ b/qutip/solver/mcsolve.py @@ -61,7 +61,7 @@ def mcsolve( defer to ``sesolve`` or ``mesolve``. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. @@ -607,9 +607,9 @@ def run( Change the ``args`` of the rhs for the evolution. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate - expectation values. Operator can be Qobj, QobjEvo or callables with - the signature `f(t: float, state: Qobj) -> Any`. + Single operator, or list or dict of operators, for which to + evaluate expectation values. Operator can be Qobj, QobjEvo or + callables with the signature `f(t: float, state: Qobj) -> Any`. timeout : float, optional Maximum time in seconds for the trajectories to run. Once this time diff --git a/qutip/solver/mesolve.py b/qutip/solver/mesolve.py index 686470b1e1..43a7c5fa13 100644 --- a/qutip/solver/mesolve.py +++ b/qutip/solver/mesolve.py @@ -88,7 +88,7 @@ def mesolve( of Liouvillian superoperators. None is equivalent to an empty list. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. diff --git a/qutip/solver/multitraj.py b/qutip/solver/multitraj.py index bbc74817ab..2c4fe80d1a 100644 --- a/qutip/solver/multitraj.py +++ b/qutip/solver/multitraj.py @@ -174,7 +174,7 @@ def run( *, args: dict[str, Any] = None, e_ops: dict[Any, Qobj | QobjEvo | Callable[[float, Qobj], Any]] = None, - target_tol: float = None, + target_tol: float | tuple[float, float] | list[tuple[float, float]] = None, timeout: float = None, seeds: int | SeedSequence | list[int | SeedSequence] = None, ) -> MultiTrajResult: diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 7615bb4d0d..adfd71c856 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -76,7 +76,7 @@ def nm_mcsolve( :func:`~qutip.core.coefficient.coefficient`. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. diff --git a/qutip/solver/sesolve.py b/qutip/solver/sesolve.py index 03d7d5dfc3..afe63e1d9c 100644 --- a/qutip/solver/sesolve.py +++ b/qutip/solver/sesolve.py @@ -64,7 +64,7 @@ def sesolve( list of times for :math:`t`. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. diff --git a/qutip/solver/stochastic.py b/qutip/solver/stochastic.py index 88f9c3ceda..d2308f7e6b 100644 --- a/qutip/solver/stochastic.py +++ b/qutip/solver/stochastic.py @@ -308,7 +308,7 @@ def _register_feedback(self, val): def smesolve( H: QobjEvoLike, rho0: Qobj, - tlist: np.typing.ArrayLike, + tlist: ArrayLike, c_ops: Qobj | QobjEvo | Sequence[QobjEvoLike] = (), sc_ops: Qobj | QobjEvo | Sequence[QobjEvoLike] = (), heterodyne: bool = False, @@ -346,7 +346,7 @@ def smesolve( List of stochastic collapse operators. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. @@ -454,7 +454,7 @@ def smesolve( def ssesolve( H: QobjEvoLike, psi0: Qobj, - tlist: np.typing.ArrayLike, + tlist: ArrayLike, sc_ops: QobjEvoLike | Sequence[QobjEvoLike] = (), heterodyne: bool = False, *, @@ -487,7 +487,7 @@ def ssesolve( List of stochastic collapse operators. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate + Single operator, or list or dict of operators, for which to evaluate expectation values. Operator can be Qobj, QobjEvo or callables with the signature `f(t: float, state: Qobj) -> Any`. @@ -746,7 +746,7 @@ def _integrate_one_traj(self, seed, tlist, result): def run_from_experiment( self, state: Qobj, - tlist: np.typing.ArrayLike, + tlist: ArrayLike, noise: Sequence[float], *, args: dict[str, Any] = None, @@ -777,9 +777,9 @@ def run_from_experiment( Arguments to pass to the Hamiltonian and collapse operators. e_ops : :obj:`.Qobj`, callable, list or dict, optional - Single, list or dict of operators for which to evaluate - expectation values. Operator can be Qobj, QobjEvo or callables with the - signature `f(t: float, state: Qobj) -> Any`. + Single operator, or list or dict of operators, for which to + evaluate expectation values. Operator can be Qobj, QobjEvo or + callables with the signature `f(t: float, state: Qobj) -> Any`. measurement : bool, default : False Whether the passed noise is the Wiener increments ``dW`` (gaussian From 67574a8840d68a7171d337ace4f77e72d04d2ada Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 17 Jul 2024 16:28:29 -0400 Subject: [PATCH 301/305] Use ArrayLike for tlist --- qutip/solver/nm_mcsolve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index adfd71c856..bdcafb437e 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -573,7 +573,7 @@ def _run_one_traj(self, seed, state, tlist, e_ops, **integrator_kwargs): def run( self, state: Qobj, - tlist: Sequence[float], + tlist: ArrayLike, ntraj: int = 1, *, args: dict[str, Any] = None, From 20dc5300d0bc481331a41621aa3a192e21c1dece Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Wed, 17 Jul 2024 16:55:43 -0400 Subject: [PATCH 302/305] Fix var name --- qutip/solver/floquet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index 02921ac594..18275b8383 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -47,7 +47,7 @@ def __init__( sparse: bool = False, sort: bool = True, precompute: ArrayLike = None, - tlist: ArrayLike = None, + times: ArrayLike = None, ): """ Parameters From 3d1f84a416607e89882853ca745f5f67501b85aa Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Fri, 19 Jul 2024 09:52:56 -0400 Subject: [PATCH 303/305] Add float to coefficientlike --- qutip/solver/nm_mcsolve.py | 2 +- qutip/typing.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index bdcafb437e..66555616fe 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -33,7 +33,7 @@ def nm_mcsolve( H: QobjEvoLike, state: Qobj, tlist: ArrayLike, - ops_and_rates: list[tuple[Qobj, float | CoefficientLike]] = (), + ops_and_rates: list[tuple[Qobj, CoefficientLike]] = (), e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, ntraj: int = 500, *, diff --git a/qutip/typing.py b/qutip/typing.py index ec2293b5f5..88186d72bb 100644 --- a/qutip/typing.py +++ b/qutip/typing.py @@ -19,6 +19,7 @@ def __call__(self, t: Real, **kwargs) -> Number: CoefficientLike = Union[ "Coefficient", + float, str, CoeffProtocol, np.ndarray, From 95fefae0c3068c1ba38bbaa94e16f3188786d20e Mon Sep 17 00:00:00 2001 From: Rochisha Agarwal Date: Sun, 21 Jul 2024 22:20:13 +0530 Subject: [PATCH 304/305] fix squeeze to make tests pass --- qutip/core/operators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutip/core/operators.py b/qutip/core/operators.py index e09613f68c..c5cd3eea9c 100644 --- a/qutip/core/operators.py +++ b/qutip/core/operators.py @@ -965,6 +965,7 @@ def squeeze( [ 0.00000000+0.j -0.30142443+0.j 0.00000000+0.j 0.95349007+0.j]] """ + dtype = dtype or settings.core["default_dtype"] or _data.Dense asq = destroy(N, offset=offset, dtype=dtype) ** 2 op = 0.5*np.conj(z)*asq - 0.5*z*asq.dag() out = op.expm(dtype=dtype) From bf5c0945e3d7516050a49d0d18044aa10d4ad26e Mon Sep 17 00:00:00 2001 From: Eric Giguere Date: Mon, 22 Jul 2024 12:50:31 -0400 Subject: [PATCH 305/305] Improve from comments --- qutip/solver/floquet.py | 8 ++++---- qutip/solver/nm_mcsolve.py | 7 ++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/qutip/solver/floquet.py b/qutip/solver/floquet.py index 18275b8383..6deb8e499f 100644 --- a/qutip/solver/floquet.py +++ b/qutip/solver/floquet.py @@ -530,7 +530,7 @@ def floquet_tensor( def fsesolve( - H: QobjEvoLike, + H: QobjEvoLike | FloquetBasis, psi0: Qobj, tlist: ArrayLike, e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, @@ -613,12 +613,12 @@ def fsesolve( def fmmesolve( - H: QobjEvoLike, + H: QobjEvoLike | FloquetBasis, rho0: Qobj, tlist: ArrayLike, c_ops: list[Qobj] = None, e_ops: EopsLike | list[EopsLike] | dict[Any, EopsLike] = None, - spectra_cb: list[Callable[[float], complex]]= None, + spectra_cb: list[Callable[[float], complex]] = None, T: float = 0.0, w_th: float = 0.0, args: dict[str, Any] = None, @@ -875,7 +875,7 @@ def _argument(self, args): if args: raise ValueError("FMESolver cannot update arguments") - def start(self, state0: Qobj, t0: float, *, floquet: bool=False) -> None: + def start(self, state0: Qobj, t0: float, *, floquet: bool = False) -> None: """ Set the initial state and time for a step evolution. ``options`` for the evolutions are read at this step. diff --git a/qutip/solver/nm_mcsolve.py b/qutip/solver/nm_mcsolve.py index 66555616fe..1931a513f3 100644 --- a/qutip/solver/nm_mcsolve.py +++ b/qutip/solver/nm_mcsolve.py @@ -212,10 +212,7 @@ def _parse_op_and_rate(op, rate, **kw): """ Sanity check the op and convert rates to coefficients. """ if not isinstance(op, Qobj): raise ValueError("NonMarkovianMCSolver ops must be of type Qobj") - if isinstance(rate, numbers.Number): - rate = ConstantCoefficient(rate) - else: - rate = coefficient(rate, **kw) + rate = coefficient(rate, **kw) return op, rate @@ -374,7 +371,7 @@ class NonMarkovianMCSolver(MCSolver): def __init__( self, H: Qobj | QobjEvo, - ops_and_rates = Sequence[tuple[Qobj, float | Coefficient]], + ops_and_rates: Sequence[tuple[Qobj, float | Coefficient]], *, options: dict[str, Any] = None, ):