Spaces:
Sleeping
Sleeping
Download tests/test_composite.py from SoulFireMage/spinor-lib: direct link, hf CLI and curl.
- Browser
- Download file 9.74 kB
-
https://huggingface.co/spaces/SoulFireMage/spinor-lib/resolve/main/tests/test_composite.py
- Command line
-
hf download hf://spaces/SoulFireMage/spinor-lib/tests/test_composite.py
-
curl -L -o test_composite.py https://huggingface.co/spaces/SoulFireMage/spinor-lib/resolve/main/tests/test_composite.py
9.74 kB
| """Tests for multi-spinor (tensor-product) states.""" | |
| import math | |
| import pytest | |
| import torch | |
| from spinor_lib import ( | |
| Spinor, | |
| bloch_vector, | |
| concurrence, | |
| density_matrix, | |
| embed_operator, | |
| entanglement_entropy, | |
| expectation, | |
| fidelity, | |
| generate_su2_rotation, | |
| identity, | |
| inner_product, | |
| kron, | |
| partial_trace, | |
| pauli, | |
| purity, | |
| reduced_density_matrix, | |
| rotate, | |
| tensor_product, | |
| von_neumann_entropy, | |
| ) | |
| C128 = torch.complex128 | |
| F64 = torch.float64 | |
| def cs(*vals, dim=2): | |
| return Spinor(torch.tensor(vals, dtype=C128), dim=dim) | |
| def random_spinor(*batch, dim=2, generator=None): | |
| return Spinor(torch.randn(*batch, dim, dtype=C128, generator=generator), dim=dim) | |
| UP, DOWN = cs(1, 0), cs(0, 1) | |
| BELL = cs(1, 0, 0, 1, dim=4) * (1 / math.sqrt(2)) # (|00> + |11>) / sqrt 2 | |
| class TestTensorProduct: | |
| def test_basis_states(self): | |
| assert torch.allclose(tensor_product(UP, DOWN).data, torch.tensor([0, 1, 0, 0], dtype=C128)) | |
| assert torch.allclose(tensor_product(DOWN, UP).data, torch.tensor([0, 0, 1, 0], dtype=C128)) | |
| def test_matches_torch_kron(self): | |
| g = torch.Generator().manual_seed(0) | |
| a, b, c = (random_spinor(generator=g) for _ in range(3)) | |
| out = tensor_product(a, b, c) | |
| assert out.dim == 8 and out.shape == (8,) | |
| assert torch.allclose(out.data, torch.kron(torch.kron(a.data, b.data), c.data)) | |
| def test_batch_broadcast(self): | |
| g = torch.Generator().manual_seed(1) | |
| a = random_spinor(4, 1, generator=g) | |
| b = random_spinor(3, generator=g) | |
| out = tensor_product(a, b) | |
| assert out.shape == (4, 3, 4) | |
| assert torch.allclose(out.data[2, 1], torch.kron(a.data[2, 0], b.data[1])) | |
| def test_norm_is_product_of_norms(self): | |
| g = torch.Generator().manual_seed(2) | |
| a, b = random_spinor(5, generator=g), random_spinor(5, generator=g) | |
| assert torch.allclose(tensor_product(a, b).norm(), a.norm() * b.norm()) | |
| def test_mixed_dims_and_dtype_promotion(self): | |
| a = Spinor(torch.tensor([1.0, 2.0, 3.0]), dim=3) # real float32 | |
| b = cs(1j, 1) | |
| out = tensor_product(a, b) | |
| assert out.dim == 6 and out.dtype == C128 | |
| def test_gradient(self): | |
| data = torch.tensor([1.0, 0.5j], dtype=C128, requires_grad=True) | |
| out = tensor_product(Spinor(data), DOWN) | |
| torch.real(inner_product(out, out)).backward() | |
| assert torch.allclose(data.grad, 2 * data.detach()) | |
| class TestKronAndEmbed: | |
| def test_kron_matches_torch(self): | |
| g = torch.Generator().manual_seed(3) | |
| A = torch.randn(2, 2, dtype=C128, generator=g) | |
| B = torch.randn(3, 3, dtype=C128, generator=g) | |
| assert torch.allclose(kron(A, B), torch.kron(A, B)) | |
| def test_kron_batched(self): | |
| g = torch.Generator().manual_seed(4) | |
| A = torch.randn(5, 2, 2, dtype=C128, generator=g) | |
| B = torch.randn(5, 2, 2, dtype=C128, generator=g) | |
| out = kron(A, B) | |
| assert out.shape == (5, 4, 4) | |
| assert torch.allclose(out[3], torch.kron(A[3], B[3])) | |
| def test_embed_operator_positions(self): | |
| sz = pauli(dtype=C128)[2] | |
| eye = identity(2, dtype=C128) | |
| assert torch.allclose(embed_operator(sz, (2, 2), 0), torch.kron(sz, eye)) | |
| assert torch.allclose(embed_operator(sz, (2, 2), 1), torch.kron(eye, sz)) | |
| assert torch.allclose(embed_operator(sz, (2, 2, 2), 1), torch.kron(torch.kron(eye, sz), eye)) | |
| def test_embed_rejects_wrong_sizes(self): | |
| with pytest.raises(ValueError): | |
| embed_operator(torch.eye(3, dtype=C128), (2, 2), 0) | |
| with pytest.raises(IndexError): | |
| embed_operator(torch.eye(2, dtype=C128), (2, 2), 2) | |
| def test_rotating_one_factor_of_a_product_state(self): | |
| g = torch.Generator().manual_seed(5) | |
| a, b = random_spinor(generator=g), random_spinor(generator=g) | |
| R = generate_su2_rotation([1, 2, 0], torch.tensor(0.9, dtype=F64)) | |
| lhs = rotate(tensor_product(a, b), embed_operator(R, (2, 2), 1)) | |
| rhs = tensor_product(a, rotate(b, R)) | |
| assert torch.allclose(lhs.data, rhs.data) | |
| def test_local_expectation_matches_single_spinor(self): | |
| g = torch.Generator().manual_seed(6) | |
| a, b = random_spinor(generator=g), random_spinor(generator=g) | |
| sx = pauli(dtype=C128)[0] | |
| ab = tensor_product(a, b).normalize() | |
| got = expectation(ab, embed_operator(sx, (2, 2), 0)) | |
| assert got.item() == pytest.approx(expectation(a.normalize(), sx).item()) | |
| class TestPartialTrace: | |
| def test_product_state_reduces_to_factors(self): | |
| g = torch.Generator().manual_seed(7) | |
| a, b = random_spinor(generator=g), random_spinor(generator=g) | |
| ab = tensor_product(a, b) | |
| assert torch.allclose(reduced_density_matrix(ab, (2, 2), 0), density_matrix(a)) | |
| assert torch.allclose(reduced_density_matrix(ab, (2, 2), 1), density_matrix(b)) | |
| def test_bell_state_reduces_to_maximally_mixed(self): | |
| rho_a = reduced_density_matrix(BELL, (2, 2), 0) | |
| assert torch.allclose(rho_a, torch.eye(2, dtype=C128) / 2) | |
| assert purity(rho_a).item() == pytest.approx(0.5) | |
| assert torch.allclose(bloch_vector(rho_a), torch.zeros(3, dtype=F64), atol=1e-12) | |
| def test_keep_everything_is_identity(self): | |
| rho = density_matrix(BELL) | |
| assert torch.allclose(partial_trace(rho, (2, 2), [0, 1]), rho) | |
| def test_three_subsystems_keep_two(self): | |
| g = torch.Generator().manual_seed(8) | |
| a, b, c = (random_spinor(generator=g) for _ in range(3)) | |
| abc = tensor_product(a, b, c) | |
| rho_ac = partial_trace(density_matrix(abc), (2, 2, 2), [0, 2]) | |
| assert rho_ac.shape == (4, 4) | |
| assert torch.allclose(rho_ac, density_matrix(tensor_product(a, c))) | |
| def test_uneven_dims_and_batch(self): | |
| g = torch.Generator().manual_seed(9) | |
| a = random_spinor(6, dim=2, generator=g) | |
| b = random_spinor(6, dim=3, generator=g) | |
| rho = density_matrix(tensor_product(a, b)) | |
| rho_b = partial_trace(rho, (2, 3), 1) | |
| assert rho_b.shape == (6, 3, 3) | |
| assert torch.allclose(rho_b, density_matrix(b)) | |
| def test_trace_is_preserved(self): | |
| g = torch.Generator().manual_seed(10) | |
| rho = density_matrix(random_spinor(4, dim=8, generator=g)) | |
| reduced = partial_trace(rho, (2, 2, 2), 1) | |
| assert torch.allclose(torch.einsum("bii->b", reduced), torch.ones(4, dtype=C128)) | |
| def test_rejects_bad_inputs(self): | |
| rho = density_matrix(BELL) | |
| with pytest.raises(ValueError): | |
| partial_trace(rho, (2, 3), 0) | |
| with pytest.raises(ValueError): | |
| partial_trace(rho, (2, 2), [0, 0]) | |
| with pytest.raises(IndexError): | |
| partial_trace(rho, (2, 2), 5) | |
| class TestEntanglement: | |
| def test_von_neumann_entropy_values(self): | |
| eye = torch.eye(2, dtype=C128) | |
| assert von_neumann_entropy(eye / 2).item() == pytest.approx(math.log(2)) | |
| assert von_neumann_entropy(eye / 2, base=2).item() == pytest.approx(1.0) | |
| assert von_neumann_entropy(density_matrix(UP)).item() == pytest.approx(0.0, abs=1e-12) | |
| def test_entanglement_entropy_bell_vs_product(self): | |
| assert entanglement_entropy(BELL, (2, 2), 0, base=2).item() == pytest.approx(1.0) | |
| product = tensor_product(cs(1, 1j), cs(2, 0.5)) | |
| assert entanglement_entropy(product, (2, 2), 0).item() == pytest.approx(0.0, abs=1e-12) | |
| def test_entanglement_entropy_symmetric_in_bipartition(self): | |
| g = torch.Generator().manual_seed(11) | |
| s = random_spinor(7, dim=4, generator=g) | |
| assert torch.allclose( | |
| entanglement_entropy(s, (2, 2), 0), entanglement_entropy(s, (2, 2), 1) | |
| ) | |
| def test_concurrence_values(self): | |
| assert concurrence(BELL).item() == pytest.approx(1.0) | |
| assert concurrence(BELL * 3).item() == pytest.approx(1.0) # normalisation-free | |
| assert concurrence(tensor_product(cs(1, 2j), cs(0.3, 1))).item() == pytest.approx(0.0, abs=1e-12) | |
| def test_concurrence_agrees_with_entropy(self): | |
| # For two-qubit pure states S = h((1 + sqrt(1 - C^2)) / 2) in bits. | |
| g = torch.Generator().manual_seed(12) | |
| s = random_spinor(20, dim=4, generator=g) | |
| C = concurrence(s) | |
| lam = (1 + torch.sqrt(1 - C**2)) / 2 | |
| h = -(lam * torch.log2(lam) + (1 - lam) * torch.log2(1 - lam)) | |
| assert torch.allclose(entanglement_entropy(s, (2, 2), 0, base=2), h, atol=1e-8) | |
| def test_concurrence_rejects_non_two_qubit(self): | |
| with pytest.raises(ValueError): | |
| concurrence(UP) | |
| def test_gradients_through_entanglement_measures(self): | |
| data = torch.tensor([1.0, 0.2, 0.3j, 0.7], dtype=C128, requires_grad=True) | |
| s = Spinor(data, dim=4) | |
| (concurrence(s) + entanglement_entropy(s, (2, 2), 0)).backward() | |
| assert torch.isfinite(data.grad).all() | |
| def test_entanglement_is_invariant_under_local_rotations(self): | |
| g = torch.Generator().manual_seed(13) | |
| s = random_spinor(5, dim=4, generator=g) | |
| R = generate_su2_rotation(torch.randn(5, 3, dtype=F64, generator=g), torch.rand(5, dtype=F64, generator=g)) | |
| local = rotate(s, embed_operator(R, (2, 2), 0)) | |
| assert torch.allclose(concurrence(local), concurrence(s)) | |
| assert torch.allclose(entanglement_entropy(local, (2, 2), 1), entanglement_entropy(s, (2, 2), 1)) | |
| def test_bell_state_via_fidelity_is_far_from_all_products(self): | |
| g = torch.Generator().manual_seed(14) | |
| products = tensor_product(random_spinor(200, generator=g), random_spinor(200, generator=g)) | |
| assert (fidelity(BELL, products) <= 0.5 + 1e-12).all() | |