openquantumsim 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openquantumsim/__init__.py +202 -0
- openquantumsim/_julia_bridge.py +80 -0
- openquantumsim/_version.py +3 -0
- openquantumsim/correlations.py +144 -0
- openquantumsim/hilbert.py +114 -0
- openquantumsim/julia/OpenQuantumSimJL/Manifest.toml +1644 -0
- openquantumsim/julia/OpenQuantumSimJL/Project.toml +26 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Correlations.jl +205 -0
- openquantumsim/julia/OpenQuantumSimJL/src/HilbertSpace.jl +51 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Lindblad.jl +155 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Observables.jl +78 -0
- openquantumsim/julia/OpenQuantumSimJL/src/OpenQuantumSimJL.jl +34 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Operators.jl +99 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Parallel.jl +1 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Propagators.jl +40 -0
- openquantumsim/julia/OpenQuantumSimJL/src/SteadyState.jl +61 -0
- openquantumsim/julia/OpenQuantumSimJL/src/TimeDep.jl +191 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Trajectories.jl +600 -0
- openquantumsim/julia/OpenQuantumSimJL/src/Utils.jl +1 -0
- openquantumsim/julia/OpenQuantumSimJL/test/runtests.jl +497 -0
- openquantumsim/observables.py +661 -0
- openquantumsim/operators.py +462 -0
- openquantumsim/phase_space.py +153 -0
- openquantumsim/plot.py +210 -0
- openquantumsim/py.typed +1 -0
- openquantumsim/result.py +332 -0
- openquantumsim/solvers.py +704 -0
- openquantumsim/sweep.py +562 -0
- openquantumsim/systems.py +136 -0
- openquantumsim/timedep.py +162 -0
- openquantumsim-0.1.0a1.dist-info/METADATA +427 -0
- openquantumsim-0.1.0a1.dist-info/RECORD +35 -0
- openquantumsim-0.1.0a1.dist-info/WHEEL +5 -0
- openquantumsim-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- openquantumsim-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""OpenQuantumSim public Python API."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .correlations import correlation_2op_1t, correlation_2op_2t
|
|
5
|
+
from .hilbert import CompositeSpace, DickeSpace, FockSpace, HilbertSpace, SpinSpace
|
|
6
|
+
from .observables import (
|
|
7
|
+
StateObservable,
|
|
8
|
+
bipartite_mutual_information,
|
|
9
|
+
bloch_observables,
|
|
10
|
+
bloch_vector,
|
|
11
|
+
bures_angle,
|
|
12
|
+
bures_distance,
|
|
13
|
+
entropy_observable,
|
|
14
|
+
evaluate_state_observables,
|
|
15
|
+
expect,
|
|
16
|
+
fidelity,
|
|
17
|
+
fidelity_observable,
|
|
18
|
+
hilbert_schmidt_distance,
|
|
19
|
+
infidelity,
|
|
20
|
+
is_density_matrix,
|
|
21
|
+
is_hermitian,
|
|
22
|
+
l1_coherence,
|
|
23
|
+
l1_coherence_observable,
|
|
24
|
+
linear_entropy,
|
|
25
|
+
linear_entropy_observable,
|
|
26
|
+
mutual_information,
|
|
27
|
+
normalize_state,
|
|
28
|
+
partial_trace,
|
|
29
|
+
partial_traces,
|
|
30
|
+
participation_ratio,
|
|
31
|
+
participation_ratio_observable,
|
|
32
|
+
population_observable,
|
|
33
|
+
population_observables,
|
|
34
|
+
populations,
|
|
35
|
+
purity,
|
|
36
|
+
purity_observable,
|
|
37
|
+
renyi_entropy,
|
|
38
|
+
state_metrics,
|
|
39
|
+
trace_distance,
|
|
40
|
+
trace_distance_observable,
|
|
41
|
+
trace_norm,
|
|
42
|
+
von_neumann_entropy,
|
|
43
|
+
)
|
|
44
|
+
from .operators import (
|
|
45
|
+
Operator,
|
|
46
|
+
basis,
|
|
47
|
+
coherent,
|
|
48
|
+
collective_excitation,
|
|
49
|
+
collective_lowering,
|
|
50
|
+
collective_raising,
|
|
51
|
+
collective_x,
|
|
52
|
+
collective_z,
|
|
53
|
+
create,
|
|
54
|
+
destroy,
|
|
55
|
+
dicke_excitation,
|
|
56
|
+
dicke_jm,
|
|
57
|
+
dicke_jp,
|
|
58
|
+
dicke_jx,
|
|
59
|
+
dicke_jz,
|
|
60
|
+
dicke_state,
|
|
61
|
+
eye,
|
|
62
|
+
fock,
|
|
63
|
+
ket2dm,
|
|
64
|
+
num,
|
|
65
|
+
sigmam,
|
|
66
|
+
sigmap,
|
|
67
|
+
sigmax,
|
|
68
|
+
sigmay,
|
|
69
|
+
sigmaz,
|
|
70
|
+
spin_jm,
|
|
71
|
+
spin_jp,
|
|
72
|
+
spin_jx,
|
|
73
|
+
spin_jz,
|
|
74
|
+
tensor,
|
|
75
|
+
thermal_dm,
|
|
76
|
+
)
|
|
77
|
+
from .phase_space import phase_space_grid, q_function, wigner
|
|
78
|
+
from .plot import (
|
|
79
|
+
expect_plot,
|
|
80
|
+
plot_density_matrix,
|
|
81
|
+
plot_expectations,
|
|
82
|
+
plot_phase_space,
|
|
83
|
+
plot_q_function,
|
|
84
|
+
plot_state_observable,
|
|
85
|
+
plot_wigner,
|
|
86
|
+
)
|
|
87
|
+
from .result import Options, QuantumSystem, Result, load_result
|
|
88
|
+
from .solvers import mcsolve, mesolve, single_trajectory, steadystate
|
|
89
|
+
from .sweep import ParameterSweep, SweepPoint, SweepRunResult
|
|
90
|
+
from .systems import JaynesCummingsSystem, MCWFSystem, jaynes_cummings_system
|
|
91
|
+
from .timedep import (
|
|
92
|
+
HamiltonianTerm,
|
|
93
|
+
InterpolatedCoefficient,
|
|
94
|
+
TimeDependentHamiltonian,
|
|
95
|
+
time_dependent_hamiltonian,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
__all__ = [
|
|
99
|
+
"__version__",
|
|
100
|
+
"CompositeSpace",
|
|
101
|
+
"DickeSpace",
|
|
102
|
+
"FockSpace",
|
|
103
|
+
"HamiltonianTerm",
|
|
104
|
+
"HilbertSpace",
|
|
105
|
+
"InterpolatedCoefficient",
|
|
106
|
+
"JaynesCummingsSystem",
|
|
107
|
+
"MCWFSystem",
|
|
108
|
+
"Operator",
|
|
109
|
+
"Options",
|
|
110
|
+
"ParameterSweep",
|
|
111
|
+
"QuantumSystem",
|
|
112
|
+
"Result",
|
|
113
|
+
"StateObservable",
|
|
114
|
+
"SpinSpace",
|
|
115
|
+
"SweepPoint",
|
|
116
|
+
"SweepRunResult",
|
|
117
|
+
"TimeDependentHamiltonian",
|
|
118
|
+
"basis",
|
|
119
|
+
"bipartite_mutual_information",
|
|
120
|
+
"bloch_vector",
|
|
121
|
+
"bloch_observables",
|
|
122
|
+
"bures_angle",
|
|
123
|
+
"bures_distance",
|
|
124
|
+
"coherent",
|
|
125
|
+
"collective_excitation",
|
|
126
|
+
"collective_lowering",
|
|
127
|
+
"collective_raising",
|
|
128
|
+
"collective_x",
|
|
129
|
+
"collective_z",
|
|
130
|
+
"correlation_2op_1t",
|
|
131
|
+
"correlation_2op_2t",
|
|
132
|
+
"create",
|
|
133
|
+
"destroy",
|
|
134
|
+
"dicke_excitation",
|
|
135
|
+
"dicke_jm",
|
|
136
|
+
"dicke_jp",
|
|
137
|
+
"dicke_jx",
|
|
138
|
+
"dicke_jz",
|
|
139
|
+
"dicke_state",
|
|
140
|
+
"entropy_observable",
|
|
141
|
+
"evaluate_state_observables",
|
|
142
|
+
"expect",
|
|
143
|
+
"expect_plot",
|
|
144
|
+
"fidelity",
|
|
145
|
+
"fidelity_observable",
|
|
146
|
+
"eye",
|
|
147
|
+
"fock",
|
|
148
|
+
"hilbert_schmidt_distance",
|
|
149
|
+
"infidelity",
|
|
150
|
+
"is_density_matrix",
|
|
151
|
+
"is_hermitian",
|
|
152
|
+
"ket2dm",
|
|
153
|
+
"jaynes_cummings_system",
|
|
154
|
+
"l1_coherence",
|
|
155
|
+
"l1_coherence_observable",
|
|
156
|
+
"linear_entropy",
|
|
157
|
+
"linear_entropy_observable",
|
|
158
|
+
"load_result",
|
|
159
|
+
"mcsolve",
|
|
160
|
+
"mesolve",
|
|
161
|
+
"mutual_information",
|
|
162
|
+
"normalize_state",
|
|
163
|
+
"num",
|
|
164
|
+
"partial_trace",
|
|
165
|
+
"partial_traces",
|
|
166
|
+
"participation_ratio",
|
|
167
|
+
"participation_ratio_observable",
|
|
168
|
+
"phase_space_grid",
|
|
169
|
+
"plot_density_matrix",
|
|
170
|
+
"plot_expectations",
|
|
171
|
+
"plot_phase_space",
|
|
172
|
+
"plot_q_function",
|
|
173
|
+
"plot_state_observable",
|
|
174
|
+
"plot_wigner",
|
|
175
|
+
"population_observable",
|
|
176
|
+
"population_observables",
|
|
177
|
+
"populations",
|
|
178
|
+
"purity",
|
|
179
|
+
"purity_observable",
|
|
180
|
+
"q_function",
|
|
181
|
+
"renyi_entropy",
|
|
182
|
+
"sigmam",
|
|
183
|
+
"sigmap",
|
|
184
|
+
"single_trajectory",
|
|
185
|
+
"spin_jm",
|
|
186
|
+
"spin_jp",
|
|
187
|
+
"spin_jx",
|
|
188
|
+
"spin_jz",
|
|
189
|
+
"sigmax",
|
|
190
|
+
"sigmay",
|
|
191
|
+
"sigmaz",
|
|
192
|
+
"steadystate",
|
|
193
|
+
"state_metrics",
|
|
194
|
+
"tensor",
|
|
195
|
+
"thermal_dm",
|
|
196
|
+
"time_dependent_hamiltonian",
|
|
197
|
+
"trace_distance",
|
|
198
|
+
"trace_distance_observable",
|
|
199
|
+
"trace_norm",
|
|
200
|
+
"von_neumann_entropy",
|
|
201
|
+
"wigner",
|
|
202
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Lazy Julia backend bridge.
|
|
2
|
+
|
|
3
|
+
The bridge intentionally imports `juliacall` lazily so ordinary Python-side
|
|
4
|
+
operator work and tests do not pay Julia startup cost.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class JuliaBridgeUnavailable(RuntimeError):
|
|
14
|
+
"""Raised when the Julia backend cannot be loaded."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_JL: Any | None = None
|
|
18
|
+
_BACKEND: Any | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def backend_path() -> Path:
|
|
22
|
+
"""Return the Julia backend package path."""
|
|
23
|
+
package_path = Path(__file__).resolve().parent / "julia" / "OpenQuantumSimJL"
|
|
24
|
+
dev_path = Path(__file__).resolve().parents[1] / "src" / "OpenQuantumSimJL"
|
|
25
|
+
if dev_path.exists():
|
|
26
|
+
return dev_path
|
|
27
|
+
return package_path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_julia() -> Any:
|
|
31
|
+
"""Return the `juliacall.Main` object, importing it on first use."""
|
|
32
|
+
global _JL
|
|
33
|
+
if _JL is not None:
|
|
34
|
+
return _JL
|
|
35
|
+
try:
|
|
36
|
+
from juliacall import Main as jl # type: ignore[import-untyped]
|
|
37
|
+
except Exception as exc: # pragma: no cover - depends on local Julia setup
|
|
38
|
+
msg = "juliacall is required to use the Julia backend."
|
|
39
|
+
raise JuliaBridgeUnavailable(msg) from exc
|
|
40
|
+
_JL = jl
|
|
41
|
+
return jl
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_backend() -> Any:
|
|
45
|
+
"""Activate and load the `OpenQuantumSimJL` backend module."""
|
|
46
|
+
global _BACKEND
|
|
47
|
+
if _BACKEND is not None:
|
|
48
|
+
return _BACKEND
|
|
49
|
+
|
|
50
|
+
jl = get_julia()
|
|
51
|
+
path = str(backend_path())
|
|
52
|
+
try:
|
|
53
|
+
jl.seval("using Pkg")
|
|
54
|
+
jl.Pkg.activate(path)
|
|
55
|
+
_instantiate_and_load_backend(jl)
|
|
56
|
+
_BACKEND = jl.OpenQuantumSimJL
|
|
57
|
+
except Exception as exc: # pragma: no cover - depends on local Julia setup
|
|
58
|
+
msg = f"Unable to load Julia backend from {path}."
|
|
59
|
+
raise JuliaBridgeUnavailable(msg) from exc
|
|
60
|
+
return _BACKEND
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _instantiate_and_load_backend(jl: Any) -> None:
|
|
64
|
+
"""Instantiate/load the backend, resolving stale manifests on retry."""
|
|
65
|
+
try:
|
|
66
|
+
jl.Pkg.instantiate()
|
|
67
|
+
jl.seval("using OpenQuantumSimJL")
|
|
68
|
+
except Exception:
|
|
69
|
+
jl.Pkg.resolve()
|
|
70
|
+
jl.Pkg.instantiate()
|
|
71
|
+
jl.seval("using OpenQuantumSimJL")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def backend_available() -> bool:
|
|
75
|
+
"""Return whether the Julia backend can be loaded."""
|
|
76
|
+
try:
|
|
77
|
+
load_backend()
|
|
78
|
+
except JuliaBridgeUnavailable:
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Two-time correlation functions via the quantum regression theorem."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from numpy.typing import NDArray
|
|
10
|
+
|
|
11
|
+
from ._julia_bridge import JuliaBridgeUnavailable, load_backend
|
|
12
|
+
from .operators import Operator
|
|
13
|
+
from .result import Options
|
|
14
|
+
|
|
15
|
+
Array = NDArray[np.complex128]
|
|
16
|
+
FloatArray = NDArray[np.float64]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def correlation_2op_1t(
|
|
20
|
+
H: Operator,
|
|
21
|
+
rho0: Array,
|
|
22
|
+
taulist: Sequence[float],
|
|
23
|
+
a_op: Operator,
|
|
24
|
+
b_op: Operator,
|
|
25
|
+
*,
|
|
26
|
+
c_ops: Sequence[Operator] | None = None,
|
|
27
|
+
options: Options | None = None,
|
|
28
|
+
) -> Array:
|
|
29
|
+
"""Return ``<A(tau) B(0)>`` using quantum regression.
|
|
30
|
+
|
|
31
|
+
This first implementation supports time-independent Lindblad systems.
|
|
32
|
+
"""
|
|
33
|
+
opts = options or Options()
|
|
34
|
+
rho0_array = np.asarray(rho0, dtype=np.complex128)
|
|
35
|
+
taus = np.asarray(taulist, dtype=np.float64)
|
|
36
|
+
c_arrays = [op.to_numpy() for op in c_ops or []]
|
|
37
|
+
_validate_correlation_inputs(H, rho0_array, taus, a_op, b_op, c_arrays, "taulist")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
backend = load_backend()
|
|
41
|
+
except JuliaBridgeUnavailable as exc:
|
|
42
|
+
msg = "correlation_2op_1t requires the Julia backend; run setup_julia.py first."
|
|
43
|
+
raise NotImplementedError(msg) from exc
|
|
44
|
+
|
|
45
|
+
raw = backend.correlation_2op_1t(
|
|
46
|
+
H.to_numpy(),
|
|
47
|
+
rho0_array,
|
|
48
|
+
taus,
|
|
49
|
+
a_op.to_numpy(),
|
|
50
|
+
b_op.to_numpy(),
|
|
51
|
+
c_arrays,
|
|
52
|
+
rtol=float(opts.rtol),
|
|
53
|
+
atol=float(opts.atol),
|
|
54
|
+
method=str(opts.method),
|
|
55
|
+
krylov_dim=int(opts.krylov_dim),
|
|
56
|
+
)
|
|
57
|
+
return np.asarray(_field(raw, "correlations"), dtype=np.complex128)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def correlation_2op_2t(
|
|
61
|
+
H: Operator,
|
|
62
|
+
rho0: Array,
|
|
63
|
+
tlist: Sequence[float],
|
|
64
|
+
taulist: Sequence[float],
|
|
65
|
+
a_op: Operator,
|
|
66
|
+
b_op: Operator,
|
|
67
|
+
*,
|
|
68
|
+
c_ops: Sequence[Operator] | None = None,
|
|
69
|
+
options: Options | None = None,
|
|
70
|
+
) -> Array:
|
|
71
|
+
"""Return ``<A(t + tau) B(t)>`` for each ``t`` and ``tau``."""
|
|
72
|
+
opts = options or Options()
|
|
73
|
+
rho0_array = np.asarray(rho0, dtype=np.complex128)
|
|
74
|
+
times = np.asarray(tlist, dtype=np.float64)
|
|
75
|
+
taus = np.asarray(taulist, dtype=np.float64)
|
|
76
|
+
c_arrays = [op.to_numpy() for op in c_ops or []]
|
|
77
|
+
_validate_correlation_inputs(H, rho0_array, times, a_op, b_op, c_arrays, "tlist")
|
|
78
|
+
_validate_nonnegative_sorted_times(taus, "taulist")
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
backend = load_backend()
|
|
82
|
+
except JuliaBridgeUnavailable as exc:
|
|
83
|
+
msg = "correlation_2op_2t requires the Julia backend; run setup_julia.py first."
|
|
84
|
+
raise NotImplementedError(msg) from exc
|
|
85
|
+
|
|
86
|
+
raw = backend.correlation_2op_2t(
|
|
87
|
+
H.to_numpy(),
|
|
88
|
+
rho0_array,
|
|
89
|
+
times,
|
|
90
|
+
taus,
|
|
91
|
+
a_op.to_numpy(),
|
|
92
|
+
b_op.to_numpy(),
|
|
93
|
+
c_arrays,
|
|
94
|
+
rtol=float(opts.rtol),
|
|
95
|
+
atol=float(opts.atol),
|
|
96
|
+
method=str(opts.method),
|
|
97
|
+
krylov_dim=int(opts.krylov_dim),
|
|
98
|
+
)
|
|
99
|
+
return np.asarray(_field(raw, "correlations"), dtype=np.complex128)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _validate_correlation_inputs(
|
|
103
|
+
H: Operator,
|
|
104
|
+
rho0: Array,
|
|
105
|
+
times: FloatArray,
|
|
106
|
+
a_op: Operator,
|
|
107
|
+
b_op: Operator,
|
|
108
|
+
c_ops: Sequence[Array],
|
|
109
|
+
time_name: str,
|
|
110
|
+
) -> None:
|
|
111
|
+
if H.shape[0] != H.shape[1]:
|
|
112
|
+
msg = "H must be square."
|
|
113
|
+
raise ValueError(msg)
|
|
114
|
+
if rho0.shape != H.shape:
|
|
115
|
+
msg = "rho0 must have the same shape as H."
|
|
116
|
+
raise ValueError(msg)
|
|
117
|
+
_validate_nonnegative_sorted_times(times, time_name)
|
|
118
|
+
for op_name, operator in (("a_op", a_op), ("b_op", b_op)):
|
|
119
|
+
if operator.shape != H.shape:
|
|
120
|
+
msg = f"{op_name} must have the same shape as H."
|
|
121
|
+
raise ValueError(msg)
|
|
122
|
+
for collapse in c_ops:
|
|
123
|
+
if collapse.shape != H.shape:
|
|
124
|
+
msg = "collapse operators must have the same shape as H."
|
|
125
|
+
raise ValueError(msg)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_nonnegative_sorted_times(times: FloatArray, name: str) -> None:
|
|
129
|
+
if times.ndim != 1 or len(times) == 0:
|
|
130
|
+
msg = f"{name} must be a non-empty one-dimensional sequence."
|
|
131
|
+
raise ValueError(msg)
|
|
132
|
+
if np.any(times < 0):
|
|
133
|
+
msg = f"{name} must be non-negative."
|
|
134
|
+
raise ValueError(msg)
|
|
135
|
+
if np.any(np.diff(times) < 0):
|
|
136
|
+
msg = f"{name} must be sorted in ascending order."
|
|
137
|
+
raise ValueError(msg)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _field(raw: object, name: str) -> object:
|
|
141
|
+
try:
|
|
142
|
+
return getattr(raw, name)
|
|
143
|
+
except AttributeError:
|
|
144
|
+
return cast(object, raw[name]) # type: ignore[index]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Hilbert-space descriptors for OpenQuantumSim."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import reduce
|
|
8
|
+
from math import isclose
|
|
9
|
+
from operator import mul
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HilbertSpace:
|
|
13
|
+
"""Base Hilbert-space descriptor."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def dim(self) -> int:
|
|
17
|
+
"""Return the finite Hilbert-space dimension."""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
def __mul__(self, other: HilbertSpace) -> CompositeSpace:
|
|
21
|
+
"""Tensor-product composition using `space_a * space_b`."""
|
|
22
|
+
if isinstance(self, CompositeSpace):
|
|
23
|
+
left = self.spaces
|
|
24
|
+
else:
|
|
25
|
+
left = (self,)
|
|
26
|
+
if isinstance(other, CompositeSpace):
|
|
27
|
+
right = other.spaces
|
|
28
|
+
else:
|
|
29
|
+
right = (other,)
|
|
30
|
+
return CompositeSpace(left + right)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class FockSpace(HilbertSpace):
|
|
35
|
+
"""Truncated bosonic Fock space with states `|0>` through `|N-1>`."""
|
|
36
|
+
|
|
37
|
+
N: int = 2
|
|
38
|
+
label: str | None = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if self.N <= 0:
|
|
42
|
+
msg = "FockSpace dimension N must be positive."
|
|
43
|
+
raise ValueError(msg)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def dim(self) -> int:
|
|
47
|
+
"""Return the truncation dimension."""
|
|
48
|
+
return self.N
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class SpinSpace(HilbertSpace):
|
|
53
|
+
"""Spin-S irreducible representation with dimension `2S + 1`."""
|
|
54
|
+
|
|
55
|
+
S: float = 0.5
|
|
56
|
+
label: str | None = None
|
|
57
|
+
|
|
58
|
+
def __post_init__(self) -> None:
|
|
59
|
+
dim = 2 * self.S + 1
|
|
60
|
+
if self.S < 0 or not isclose(dim, round(dim), rel_tol=0.0, abs_tol=1e-12):
|
|
61
|
+
msg = "SpinSpace requires S >= 0 and 2S + 1 to be an integer."
|
|
62
|
+
raise ValueError(msg)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def dim(self) -> int:
|
|
66
|
+
"""Return `2S + 1`."""
|
|
67
|
+
return int(round(2 * self.S + 1))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class DickeSpace(HilbertSpace):
|
|
72
|
+
"""Permutation-symmetric Dicke manifold for ``n_spins`` two-level systems."""
|
|
73
|
+
|
|
74
|
+
n_spins: int
|
|
75
|
+
label: str | None = None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
if self.n_spins < 0:
|
|
79
|
+
msg = "DickeSpace requires n_spins >= 0."
|
|
80
|
+
raise ValueError(msg)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def dim(self) -> int:
|
|
84
|
+
"""Return the symmetric-manifold dimension ``n_spins + 1``."""
|
|
85
|
+
return self.n_spins + 1
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def total_spin(self) -> float:
|
|
89
|
+
"""Return the collective spin ``S = n_spins / 2``."""
|
|
90
|
+
return self.n_spins / 2
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class CompositeSpace(HilbertSpace):
|
|
95
|
+
"""Tensor product of multiple Hilbert spaces."""
|
|
96
|
+
|
|
97
|
+
spaces: tuple[HilbertSpace, ...]
|
|
98
|
+
label: str | None = None
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
if len(self.spaces) == 0:
|
|
102
|
+
msg = "CompositeSpace requires at least one subsystem."
|
|
103
|
+
raise ValueError(msg)
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def dim(self) -> int:
|
|
107
|
+
"""Return the product dimension."""
|
|
108
|
+
return reduce(mul, (space.dim for space in self.spaces), 1)
|
|
109
|
+
|
|
110
|
+
def __iter__(self) -> Iterator[HilbertSpace]:
|
|
111
|
+
return iter(self.spaces)
|
|
112
|
+
|
|
113
|
+
def __len__(self) -> int:
|
|
114
|
+
return len(self.spaces)
|