From 3912291c572126479405f6327bb35d1763215d57 Mon Sep 17 00:00:00 2001 From: Johannes Hauschild Date: Wed, 28 Oct 2026 12:22:20 +0100 Subject: [PATCH 1/2] refactor label handling --- docs/cpp/tensors/labels.rst | 6 +- docs/cpp_conversion/convert_LabelledLegs.md | 5 + docs/cpp_conversion/convert_LegLabels.md | 65 +++ docs/intro/from_np_conserved.rst | 21 +- docs/python/lasyten.tensors.rst | 6 +- include/lasyten/symmetries/spaces.h | 25 +- include/lasyten/tensors/charged_tensor.h | 15 +- include/lasyten/tensors/constructors.h | 14 +- include/lasyten/tensors/decompositions.h | 18 +- include/lasyten/tensors/diagonal_tensor.h | 44 +- include/lasyten/tensors/helpers.h | 12 +- include/lasyten/tensors/hidden_leg_tensor.h | 17 +- include/lasyten/tensors/labels.h | 176 +++--- include/lasyten/tensors/mask.h | 12 +- include/lasyten/tensors/planar.h | 20 +- include/lasyten/tensors/sparse.h | 7 +- include/lasyten/tensors/symmetric_tensor.h | 48 +- include/lasyten/tensors/tensor.h | 37 +- include/lasyten/tools/hdf5_export.h | 9 +- lasyten/tensors/__init__.py | 4 +- lasyten/tensors/_tensors.py | 4 +- lasyten/tensors/sparse.py | 16 +- lasyten/testing/random_generation.py | 2 +- pybind/symmetries/py_spaces.cpp | 10 +- pybind/tensors/py_charged_tensor.cpp | 22 - pybind/tensors/py_constructors.cpp | 10 +- pybind/tensors/py_decompositions.cpp | 8 +- pybind/tensors/py_diagonal_tensor.cpp | 2 +- pybind/tensors/py_factory_parse.hpp | 2 +- pybind/tensors/py_helpers.cpp | 6 +- pybind/tensors/py_labels.cpp | 178 ++++--- pybind/tensors/py_ops_legs.cpp | 4 +- pybind/tensors/py_planar.cpp | 25 +- pybind/tensors/py_sparse.cpp | 16 +- pybind/tensors/py_symmetric_tensor.cpp | 6 +- pybind/tensors/py_tensor.cpp | 43 +- pybind/tensors/py_trampolines.hpp | 5 - src/models/couplings.cpp | 47 +- src/models/degrees_of_freedom.cpp | 20 +- src/models/sites.cpp | 2 +- src/symmetries/spaces.cpp | 46 +- src/tensors/charged_tensor.cpp | 83 ++- src/tensors/constructors.cpp | 61 ++- src/tensors/decompositions.cpp | 188 +++---- src/tensors/diagonal_tensor.cpp | 69 +-- src/tensors/helpers.cpp | 55 +- src/tensors/hidden_leg_tensor.cpp | 117 ++-- src/tensors/labels.cpp | 502 ++++++++++-------- src/tensors/mask.cpp | 42 +- src/tensors/ops_algebra.cpp | 177 +++--- src/tensors/ops_elementwise.cpp | 8 +- src/tensors/ops_legs.cpp | 181 ++++--- src/tensors/planar.cpp | 172 +++--- src/tensors/sparse.cpp | 16 +- src/tensors/symmetric_tensor.cpp | 66 +-- src/tensors/tensor.cpp | 88 +-- src/tools/hdf5_export.cpp | 21 +- .../backends/test_fusion_tree_backend.py | 4 +- tests/python_tests/models/test_couplings.py | 18 +- tests/python_tests/test_planar.py | 62 +-- tests/python_tests/test_spaces.py | 16 +- tests/python_tests/test_sparse.py | 4 +- tests/python_tests/test_tensors.py | 106 ++-- toycodes/tenpy_toycodes/a_mps.py | 4 +- toycodes/tenpy_toycodes/d_dmrg.py | 4 +- 65 files changed, 1629 insertions(+), 1470 deletions(-) create mode 100644 docs/cpp_conversion/convert_LegLabels.md diff --git a/docs/cpp/tensors/labels.rst b/docs/cpp/tensors/labels.rst index 9b69ca75..c3cbf33f 100644 --- a/docs/cpp/tensors/labels.rst +++ b/docs/cpp/tensors/labels.rst @@ -3,10 +3,10 @@ labels.h Declared in ``include/lasyten/tensors/labels.h``. -lasyten::LabelledLegs -------------------- +lasyten::LegLabels +---------------- -.. doxygenclass:: lasyten::LabelledLegs +.. doxygenclass:: lasyten::LegLabels :project: lasyten :members: :undoc-members: diff --git a/docs/cpp_conversion/convert_LabelledLegs.md b/docs/cpp_conversion/convert_LabelledLegs.md index 89d8a5d5..62a0c4ef 100644 --- a/docs/cpp_conversion/convert_LabelledLegs.md +++ b/docs/cpp_conversion/convert_LabelledLegs.md @@ -1,5 +1,10 @@ # Conversion of LabelledLegs (+ label helpers) +> **Rename:** `LabelledLegs` was refactored into composed `LegLabels` +> (`Tensor.labels`). For the mechanical call-site / type replacement summary used +> by open PRs, see [convert_LegLabels.md](convert_LegLabels.md). This file keeps +> the historical batch-1–2 conversion notes. + ## metadata - original python name: `LabelledLegs` (+ helpers in same batch) diff --git a/docs/cpp_conversion/convert_LegLabels.md b/docs/cpp_conversion/convert_LegLabels.md new file mode 100644 index 00000000..36e3b431 --- /dev/null +++ b/docs/cpp_conversion/convert_LegLabels.md @@ -0,0 +1,65 @@ +# Conversion note: `LabelledLegs` → `LegLabels` (composition) + +Mechanical replacement summary for open tenpy / lasyten branches and PRs that still +touch the old label API. Prefer this file over +[convert_LabelledLegs.md](convert_LabelledLegs.md) (historical batch notes). + +## Type / MRO + +- Rename class `LabelledLegs` → `LegLabels`. +- `Tensor` / `TensorPlaceholder` **compose** a `LegLabels labels` member; they no + longer inherit `LabelledLegs`. +- Keep `tensor.num_legs` on `Tensor` (synced with `labels.size()`). + +## Label values + +| Old | New | +| --- | --- | +| `OptionalLabel` / `std::optional` | `std::string` | +| `OptionalLabels` | `std::vector` (`LegLabelList`) | +| `std::nullopt` / Python `None` as unset | `""` | +| `if (lab)` / `lab.has_value()` | `!lab.empty()` | + +## Call-site map (Python) + +| Old | New | +| --- | --- | +| `LabelledLegs` | `LegLabels` | +| `t.labels` / `t.labels()` as a list | `t.labels` (`LegLabels`); copy via `t.labels.as_list()` | +| `t.set_labels(x)` | `t.labels.set(x)` or `t.labels = x` | +| `t.set_label(i, x)` | `t.labels[i] = x` | +| `t.relabel(m)` | `t.labels.update(m)` | +| `t.get_leg_idcs(x)[0]` | `t.labels.index(x)` | +| `t.get_leg_idcs([...])` | `t.labels.indices([...])` | +| `t.has_label(...)` | `t.labels.has(...)` or `'x' in t.labels` | +| `t.labels_are(...)` | `t.labels.are(...)` | +| `t.is_fully_labelled` | `t.labels.is_fully_labelled` | +| `t._labels` / `t._labelmap` | remove (compare `t.labels` or use `as_list()`) | +| `for x in t.labels` / `list(t.labels)` | `for x in t.labels.as_list()` / `t.labels.as_list()` | +| `assert t.labels == [a, None, b]` | `assert t.labels == [a, '', b]` | + +`LegLabels` supports equality, `len`, `[]` get/set (int or str key), and +assignment from a sequence. It does **not** implement `__iter__`. + +`relabel1` / `relabel2` kwargs on `compose` / `tdot` / `outer` / etc. are unchanged +(function arguments, not tensor methods). + +Chaining note: old `t.relabel(m)` returned `self`; `t.labels.update(m)` returns +the `LegLabels` member. Split chains: + +```python +# old: expect = move_leg(...).relabel({...}) +expect = move_leg(...) +expect.labels.update({...}) +``` + +## HDF5 + +- Nested `"labels"` stores a string list; load old `None` entries as `""`. +- Standalone roundtrips: `LegLabels::{save_hdf5,from_hdf5}`. + +## Bindings / stubs + +- `pybind/tensors/py_labels.cpp`: bind `LegLabels`; sequences of `str` only. +- Drop nested `labels` property and `_labelmap` from the old holder API. +- `lasyten/_core.pyi`: `list[str]`, not `list[str | None]`. diff --git a/docs/intro/from_np_conserved.rst b/docs/intro/from_np_conserved.rst index 5ea27e53..baf6d061 100644 --- a/docs/intro/from_np_conserved.rst +++ b/docs/intro/from_np_conserved.rst @@ -270,12 +270,15 @@ Differences: - Dual labels use a trailing ``*``, same as TeNPy ``Array.conj()``. Combined pipes still use ``(a.b)``. Avoid ``( ) . ? ! *`` inside a *single* (not combined/dual) label; see :func:`~lasyten.tensors.is_valid_leg_label`. -- Set labels with :meth:`~lasyten.tensors.LabelledLegs.set_labels` / - :meth:`~lasyten.tensors.LabelledLegs.set_label` / - :meth:`~lasyten.tensors.LabelledLegs.relabel` (in-place). There is no - ``iset_leg_labels``. -- Read them from :attr:`~lasyten.tensors.LabelledLegs.labels`, - :attr:`~lasyten.tensors.Tensor.codomain_labels`, +- Unset labels are empty strings (``""``), not ``None``. +- Each tensor owns a :class:`~lasyten.tensors.LegLabels` member + ``t.labels``. Assign a full list with ``t.labels = [...]`` or + ``t.labels.set([...])``, rename with ``t.labels.update({...})``, and set + one entry with ``t.labels[i] = ...`` or ``t.labels['old'] = 'new'``. There + is no ``iset_leg_labels`` / ``set_labels`` / ``relabel``. +- Compare or index via ``t.labels``; use ``t.labels.as_list()`` when a real + Python list is required (``LegLabels`` is not iterable). Read slices with + :attr:`~lasyten.tensors.Tensor.codomain_labels` / :attr:`~lasyten.tensors.Tensor.domain_labels`. There is no ``get_leg_labels()``. - Duplicate labels after a contraction are dropped (as in TeNPy). Optional @@ -285,9 +288,9 @@ Differences: .. code-block:: python - A.set_labels(['p', 'vL', 'vR']) # TeNPy: A.iset_leg_labels(...) - A.relabel({'vL': 'vR', 'vR': 'vL'}) - A.set_label(0, 'p0') + A.labels = ['p', 'vL', 'vR'] # TeNPy: A.iset_leg_labels(...) + A.labels.update({'vL': 'vR', 'vR': 'vL'}) + A.labels[0] = 'p0' Contractions diff --git a/docs/python/lasyten.tensors.rst b/docs/python/lasyten.tensors.rst index f046c54b..07dc0096 100644 --- a/docs/python/lasyten.tensors.rst +++ b/docs/python/lasyten.tensors.rst @@ -22,10 +22,10 @@ Classes and functions imported from the pybind11 module :mod:`lasyten._core`. .. lasyten-inheritance-diagram:: -lasyten.tensors.LabelledLegs --------------------------- +lasyten.tensors.LegLabels +----------------------- -.. autoclass:: lasyten.tensors.LabelledLegs +.. autoclass:: lasyten.tensors.LegLabels :members: :undoc-members: :show-inheritance: diff --git a/include/lasyten/symmetries/spaces.h b/include/lasyten/symmetries/spaces.h index eb325c32..67ee4d21 100644 --- a/include/lasyten/symmetries/spaces.h +++ b/include/lasyten/symmetries/spaces.h @@ -400,9 +400,8 @@ class ElementarySpace /// Direct sum with `others`, always wrapped as a `DirectSumSpace` (even if `others` /// is empty and no `summand_labels` are given). - [[nodiscard]] Ptr direct_sum( - std::vector const& others, - std::optional summand_labels = std::nullopt) const; + [[nodiscard]] Ptr direct_sum(std::vector const& others, + std::optional summand_labels = std::nullopt) const; Space::Ptr drop_symmetry(std::optional> which = std::nullopt) override; @@ -473,11 +472,11 @@ class DirectSumSpace : public ElementarySpace std::vector spaces; /// Optional label per summand (parallel to `spaces`; `nullopt` entries are unlabeled). - OptionalLabels summand_labels; + LegLabelList summand_labels; explicit DirectSumSpace(std::vector spaces, bool is_dual = false, - std::optional summand_labels = std::nullopt); + std::optional summand_labels = std::nullopt); ~DirectSumSpace() override = default; void test_sanity() const override; @@ -491,7 +490,7 @@ class DirectSumSpace : public ElementarySpace [[nodiscard]] static Ptr from_spaces( std::vector spaces, bool is_dual = false, - std::optional summand_labels = std::nullopt); + std::optional summand_labels = std::nullopt); /// Resolve a summand reference (index or label) to a normalized, non-negative index. [[nodiscard]] int64 get_summand_idx(SummandRef which) const; @@ -558,7 +557,7 @@ class DirectSumSpace : public ElementarySpace std::optional> keep_indices = std::nullopt, std::optional> discard_indices = std::nullopt, std::shared_ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt) const; /// Inclusion Mask onto the union of selected summands (dagger of the projection). @@ -573,7 +572,7 @@ class DirectSumSpace : public ElementarySpace std::optional> keep_indices = std::nullopt, std::optional> discard_indices = std::nullopt, std::shared_ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt) const; /// Projection Mask onto summand ``which`` (index or label). @@ -586,7 +585,7 @@ class DirectSumSpace : public ElementarySpace [[nodiscard]] MaskPtr projection_onto_summand( SummandRef which, std::shared_ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt) const; /// Inclusion Mask of summand ``which`` (index or label; dagger of the projection). @@ -596,7 +595,7 @@ class DirectSumSpace : public ElementarySpace [[nodiscard]] MaskPtr inclusion_of_summand( SummandRef which, std::shared_ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt) const; /// Unit vector selecting summand ``i``. @@ -608,7 +607,7 @@ class DirectSumSpace : public ElementarySpace [[nodiscard]] SymmetricTensorPtr unit_vector_of_summand( int64 i, std::shared_ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt) const; @@ -644,12 +643,12 @@ class DirectSumSpace : public ElementarySpace SectorArray defining_sectors; std::vector multiplicities; std::optional> basis_perm; - OptionalLabels summand_labels; + LegLabelList summand_labels; }; static Prepared prepare(std::vector spaces, bool is_dual, - std::optional summand_labels); + std::optional summand_labels); DirectSumSpace(Prepared prepared, bool is_dual); diff --git a/include/lasyten/tensors/charged_tensor.h b/include/lasyten/tensors/charged_tensor.h index 35f037b1..3f7df8f2 100644 --- a/include/lasyten/tensors/charged_tensor.h +++ b/include/lasyten/tensors/charged_tensor.h @@ -66,8 +66,8 @@ class ChargedTensor : public Tensor std::variant charge); /// Utility like `_init_parse_labels`, but also returns invariant part labels. - [[nodiscard]] static std::tuple _parse_inv_labels( - std::optional labels, + [[nodiscard]] static std::tuple _parse_inv_labels( + std::optional labels, TensorProduct::Ptr const& codomain, TensorProduct::Ptr const& domain); @@ -83,7 +83,7 @@ class ChargedTensor : public Tensor TensorProduct::Ptr domain = nullptr, BlockBackend::BlockPtr charged_state = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -94,7 +94,7 @@ class ChargedTensor : public Tensor TensorProduct::Ptr domain = nullptr, std::optional> charge = std::nullopt, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt, float64 tol = 1e-6, @@ -128,7 +128,7 @@ class ChargedTensor : public Tensor std::variant charge, BlockBackend::BlockPtr charged_state = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Complex128, std::optional device = std::nullopt); @@ -206,11 +206,6 @@ class ChargedTensor : public Tensor std::string const& indent, bool use_symm_str = false) const override; - /// Set a single label at given position, in-place. Return the modified instance. - LabelledLegs& set_label(int64 pos, OptionalLabel label) override; - /// Set the given labels, in-place. Return the modified instance. - Tensor& set_labels(OptionalLabels labels) override; - /// Convert to a tensor with a different backend. /// /// @param backend The backend of the result. diff --git a/include/lasyten/tensors/constructors.h b/include/lasyten/tensors/constructors.h index fcda6ea8..1848e8f7 100644 --- a/include/lasyten/tensors/constructors.h +++ b/include/lasyten/tensors/constructors.h @@ -26,7 +26,7 @@ namespace lasyten { /// @param diagonal If true, return a diagonal identity. [[nodiscard]] TensorPtr eye(Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Float64, std::optional device = std::nullopt, bool diagonal = true); @@ -43,7 +43,7 @@ namespace lasyten { TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -60,7 +60,7 @@ namespace lasyten { TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt, bool understood_braiding = false); @@ -92,7 +92,7 @@ namespace lasyten { std::optional legs_pos = std::nullopt, std::optional codomain_pos = std::nullopt, std::optional domain_pos = std::nullopt, - OptionalLabel label = std::nullopt, + std::string label = "", bool is_dual = false); /// Return a zero tensor with the same type, dtype, legs, backend and labels. @@ -142,10 +142,10 @@ namespace lasyten { /// on the resulting domain leg; `nullopt` means unlabeled. If given, must have one entry /// per column. [[nodiscard]] TensorPtr tensor_from_grid(std::vector> grid, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, - std::optional row_labels = std::nullopt, - std::optional col_labels = std::nullopt); + std::optional row_labels = std::nullopt, + std::optional col_labels = std::nullopt); /// Extract cell ``(row, col)`` from a tensor stacked by `tensor_from_grid`. /// diff --git a/include/lasyten/tensors/decompositions.h b/include/lasyten/tensors/decompositions.h index 60b5dea0..bd128743 100644 --- a/include/lasyten/tensors/decompositions.h +++ b/include/lasyten/tensors/decompositions.h @@ -72,7 +72,7 @@ namespace lasyten { /// @returns `(W, V)`: real eigenvalues and orthonormal eigenvectors. [[nodiscard]] std::tuple eigh( TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -97,7 +97,7 @@ namespace lasyten { /// @returns `(W, V)`: eigenvalues and right eigenvectors. [[nodiscard]] std::tuple eig( TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -114,7 +114,7 @@ namespace lasyten { /// details. Defaults to `nullopt`, which is the same as `"<"`. /// @returns `W`: real eigenvalues as a `DiagonalTensor`. [[nodiscard]] DiagonalTensorPtr eigvalsh(TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -130,7 +130,7 @@ namespace lasyten { /// details. Defaults to `nullopt`, which leaves the backend default order (unsorted). /// @returns `W`: (generally complex) eigenvalues as a `DiagonalTensor`. [[nodiscard]] DiagonalTensorPtr eigvals(TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -188,7 +188,7 @@ namespace lasyten { /// @returns `(L, Q)`. [[nodiscard]] std::tuple lq( TensorCPtr tensor, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, bool charge_leg_top = true); @@ -224,7 +224,7 @@ namespace lasyten { /// @returns `(Q, R)`. [[nodiscard]] std::tuple qr( TensorCPtr tensor, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, bool charge_leg_top = true); @@ -274,7 +274,7 @@ namespace lasyten { /// @returns `(U, S, Vh)`. [[nodiscard]] std::tuple svd( TensorCPtr tensor, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, bool charge_leg_top = true, std::optional algorithm = std::nullopt); @@ -337,7 +337,7 @@ svd_apply_mask(TensorCPtr U, DiagonalTensorCPtr S, TensorCPtr Vh, MaskCPtr mask) float64 trunc_cut = 0., float64 svd_min = 0., bool minimize_error = true, - std::optional mask_labels = std::nullopt); + std::optional mask_labels = std::nullopt); /// Truncated version of `svd`. /// @@ -373,7 +373,7 @@ svd_apply_mask(TensorCPtr U, DiagonalTensorCPtr S, TensorCPtr Vh, MaskCPtr mask) /// See also: `svd`. [[nodiscard]] std::tuple truncated_svd( TensorCPtr tensor, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, bool charge_leg_top = true, std::optional algorithm = std::nullopt, diff --git a/include/lasyten/tensors/diagonal_tensor.h b/include/lasyten/tensors/diagonal_tensor.h index 5c40f7c5..97661c50 100644 --- a/include/lasyten/tensors/diagonal_tensor.h +++ b/include/lasyten/tensors/diagonal_tensor.h @@ -55,7 +55,7 @@ class DiagonalTensor : public SymmetricTensor Space::Ptr leg, TensorBackend::Ptr backend, Symmetry::Ptr symmetry, - OptionalLabels labels); + LegLabelList labels); ~DiagonalTensor() override = default; @@ -97,7 +97,7 @@ class DiagonalTensor : public SymmetricTensor [[nodiscard]] static Ptr from_block_func(BlockFactoryFn func, Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -105,7 +105,7 @@ class DiagonalTensor : public SymmetricTensor [[nodiscard]] static Ptr from_dense_block(BlockBackend::BlockPtr block, Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, float64 tol = 1e-6, std::optional device = std::nullopt, @@ -124,7 +124,7 @@ class DiagonalTensor : public SymmetricTensor [[nodiscard]] static Ptr from_diag_block(BlockBackend::BlockPtr diag, Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt, float64 tol = 1e-6); @@ -136,7 +136,7 @@ class DiagonalTensor : public SymmetricTensor /// @param device The device of the tensor. If omitted, use the default device of the backend. [[nodiscard]] static Ptr from_eye(Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Float64, std::optional device = std::nullopt); @@ -155,14 +155,13 @@ class DiagonalTensor : public SymmetricTensor /// @param sigma The standard deviation of the distribution /// @param dtype The dtype for the entries. /// @param device The device of the tensor. If omitted, use the default device of the backend. - [[nodiscard]] static Ptr from_random_normal( - Space::Ptr leg = nullptr, - TensorCPtr mean = nullptr, - float64 sigma = 1.0, - TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, - Dtype dtype = Dtype::Complex128, - std::optional device = std::nullopt); + [[nodiscard]] static Ptr from_random_normal(Space::Ptr leg = nullptr, + TensorCPtr mean = nullptr, + float64 sigma = 1.0, + TensorBackend::Ptr backend = nullptr, + std::optional labels = std::nullopt, + Dtype dtype = Dtype::Complex128, + std::optional device = std::nullopt); /// Generate a tensor with uniformly random block-entries. /// @@ -178,12 +177,11 @@ class DiagonalTensor : public SymmetricTensor /// @param leg, backend, labels Arguments for constructor of `DiagonalTensor`. /// @param dtype The dtype for the entries. /// @param device The device of the tensor. If omitted, use the default device of the backend. - [[nodiscard]] static Ptr from_random_uniform( - Space::Ptr leg, - TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, - Dtype dtype = Dtype::Complex128, - std::optional device = std::nullopt); + [[nodiscard]] static Ptr from_random_uniform(Space::Ptr leg, + TensorBackend::Ptr backend = nullptr, + std::optional labels = std::nullopt, + Dtype dtype = Dtype::Complex128, + std::optional device = std::nullopt); /// Initialize a `SymmetricTensor` by generating its blocks from a function. /// @@ -215,7 +213,7 @@ class DiagonalTensor : public SymmetricTensor SectorBlockFactoryFn func, Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -236,7 +234,7 @@ class DiagonalTensor : public SymmetricTensor /// backend. [[nodiscard]] static Ptr from_zero(Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Complex128, std::optional device = std::nullopt); @@ -424,7 +422,7 @@ class Identity : public DiagonalTensor Identity(Space::Ptr leg, TensorBackend::Ptr backend, Symmetry::Ptr symmetry, - OptionalLabels labels, + LegLabelList labels, Dtype dtype, std::string device); @@ -445,7 +443,7 @@ class Identity : public DiagonalTensor /// @param device The device of the tensor. If omitted, use the default device of the backend. [[nodiscard]] static Ptr from_eye(Space::Ptr leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Float64, std::optional device = std::nullopt); diff --git a/include/lasyten/tensors/helpers.h b/include/lasyten/tensors/helpers.h index b35e2692..006d741b 100644 --- a/include/lasyten/tensors/helpers.h +++ b/include/lasyten/tensors/helpers.h @@ -129,14 +129,14 @@ _decomposition_prepare(TensorCPtr tensor, bool new_leg_dual); /// /// @param new_labels Label sequence for the two new legs. /// @returns Pair of leg labels for the decomposition factors. -[[nodiscard]] std::pair _decomposition_labels( - OptionalLabels const& new_labels); +[[nodiscard]] std::pair _decomposition_labels( + LegLabelList const& new_labels); -/// Parse labels for `svd`. `nullopt` means all-unlabelled. +/// Parse labels for `svd`. Outer `nullopt` means all-unlabelled (empty strings). /// -/// @param new_labels Optional label sequence; `nullopt` means all unlabeled. +/// @param new_labels Optional label sequence; `nullopt` means all unlabeled (`""`). /// @returns Four leg labels for the SVD factors. -[[nodiscard]] std::tuple -_svd_new_labels(std::optional new_labels); +[[nodiscard]] std::tuple _svd_new_labels( + std::optional new_labels); } // namespace lasyten diff --git a/include/lasyten/tensors/hidden_leg_tensor.h b/include/lasyten/tensors/hidden_leg_tensor.h index 0b1b7a6a..d14ab78f 100644 --- a/include/lasyten/tensors/hidden_leg_tensor.h +++ b/include/lasyten/tensors/hidden_leg_tensor.h @@ -54,22 +54,22 @@ class HiddenLegTensor : public SymmetricTensor /// True for ChargedTensor charge markers and short-lived compose temps (``!``, ``!1``, /// ``!A``). - [[nodiscard]] static bool is_charge_temp_label(OptionalLabel const& label); + [[nodiscard]] static bool is_charge_temp_label(std::string const& label); /// True if `label` is a user-facing hidden leg label (``!`` prefix, not a charge temp). - [[nodiscard]] static bool is_hidden_leg_label(OptionalLabel const& label); + [[nodiscard]] static bool is_hidden_leg_label(std::string const& label); /// True if any label starts with ``!``. - [[nodiscard]] static bool has_hidden_leg_labels(OptionalLabels const& labels); + [[nodiscard]] static bool has_hidden_leg_labels(LegLabelList const& labels); /// Strip a leading ``!`` from a hidden label. Returns `label` unchanged if not hidden. - [[nodiscard]] static OptionalLabel strip_hidden_prefix(OptionalLabel const& label); + [[nodiscard]] static std::string strip_hidden_prefix(std::string const& label); /// Prefix ``!`` to a non-hidden label. Raises if already hidden or empty. [[nodiscard]] static std::string add_hidden_prefix(std::string const& label); /// Raise if hidden labels contain duplicates or a dual pair (`!a` and `!a*`). - static void validate_no_dual_hidden_pair(OptionalLabels const& labels); + static void validate_no_dual_hidden_pair(LegLabelList const& labels); /// Indices of legs whose labels start with ``!``. [[nodiscard]] std::vector hidden_leg_idcs() const; @@ -98,9 +98,6 @@ class HiddenLegTensor : public SymmetricTensor std::optional dtype = std::nullopt, std::optional device = std::nullopt) override; - LabelledLegs& set_label(int64 pos, OptionalLabel label) override; - Tensor& set_labels(OptionalLabels labels) override; - /// Import from hdf5 [[nodiscard]] static Ptr from_hdf5(lasyten::hdf5::Loader& loader, HighFive::Group& h5gr, @@ -116,9 +113,9 @@ class HiddenLegTensor : public SymmetricTensor }; /// True if any label contains the character ``!`` (not necessarily as a prefix). -[[nodiscard]] bool label_contains_exclamation(OptionalLabel const& label); +[[nodiscard]] bool label_contains_exclamation(std::string const& label); /// Raise if any label contains ``!``. Used by normal (non-HiddenLeg) tensors. -void reject_exclamation_in_labels(OptionalLabels const& labels, std::string const& context); +void reject_exclamation_in_labels(LegLabelList const& labels, std::string const& context); } // namespace lasyten diff --git a/include/lasyten/tensors/labels.h b/include/lasyten/tensors/labels.h index 21be6c68..b6ea3c20 100644 --- a/include/lasyten/tensors/labels.h +++ b/include/lasyten/tensors/labels.h @@ -2,20 +2,19 @@ #include #include +#include +#include #include #include #include -#include #include #include namespace lasyten { -/// Optional string label for a tensor leg (`nullopt` when unlabeled). -using OptionalLabel = std::optional; -/// Flat list of leg labels in `legs` order. -using OptionalLabels = std::vector; +/// Flat list of leg labels in `legs` order. Empty string means unset. +using LegLabelList = std::vector; /// Reserved character to indicate contractions in `lasyten.planar` diagrams. inline constexpr char CONTRACT_SYMBOL = '@'; @@ -29,106 +28,153 @@ inline constexpr char const* FORBIDDEN_LEG_LABEL_CHARS[] = { " ", "\t", "\n", "@", ":", "-", ">", }; -/// If the given string is a valid leg label. -[[nodiscard]] bool is_valid_leg_label(OptionalLabel const& label); +/// If the given string is a valid leg label (`""` is always valid = unset). +[[nodiscard]] bool is_valid_leg_label(std::string const& label); /// The label that a combined leg should have. /// /// @param labels Labels of the legs being combined. /// @param offset Added to the index used for anonymous `?n` placeholders. /// Defaults to `0` (needed by `_dual_leg_label` for nested combined labels). -[[nodiscard]] std::string _combine_leg_labels(OptionalLabels const& labels, int64 offset = 0); +[[nodiscard]] std::string _combine_leg_labels(LegLabelList const& labels, int64 offset = 0); /// Undo `_combine_leg_labels`, i.e. recover the original labels. /// -/// @param label Combined label, or `nullopt` for an anonymous group. -/// @param num Required length when `label` is `nullopt`; otherwise optional. -[[nodiscard]] OptionalLabels _split_leg_label(OptionalLabel const& label, - std::optional num = std::nullopt); +/// @param label Combined label, or `""` for an anonymous group. +/// @param num Required length when `label` is empty; otherwise optional. +[[nodiscard]] LegLabelList _split_leg_label(std::string const& label, + std::optional num = std::nullopt); /// The label that a leg should have after conjugation. -[[nodiscard]] OptionalLabel _dual_leg_label(OptionalLabel const& label); +[[nodiscard]] std::string _dual_leg_label(std::string const& label); /// Dual labels in reversed order (helper for conjugated combined labels). -[[nodiscard]] OptionalLabels _dual_label_list(OptionalLabels const& labels); +[[nodiscard]] LegLabelList _dual_label_list(LegLabelList const& labels); /// Utility function to combine two lists of labels that should match. /// /// Per pair of labels: -/// - If one is unset (`nullopt`), use the other. +/// - If one is unset (`""`), use the other. /// - If they are equal, use that label. -/// - If they are different, emit a DEBUG message to the logger and choose -/// `nullopt`. +/// - If they are different, emit a DEBUG message to the logger and choose `""`. /// /// @param labels1,labels2 Label lists that should match pairwise. /// @returns Combined labels (length of the shorter input). -[[nodiscard]] OptionalLabels _get_matching_labels(OptionalLabels const& labels1, - OptionalLabels const& labels2); +[[nodiscard]] LegLabelList _get_matching_labels(LegLabelList const& labels1, + LegLabelList const& labels2); -/// Base class that implements handling of labelled legs. -class LabelledLegs +/// Handles labelled legs for tensors (composition, not inheritance). +/// +/// Unset labels are empty strings. Behaves list-like for equality, indexing, and +/// assignment from `LegLabelList`, without full iterator support. +class LegLabels { public: - using Ptr = std::shared_ptr; + /// Called before a full or single-label assignment with the proposed full list. + /// May throw to reject the mutation. Does not own `this`. + using BeforeHook = std::function; + /// Called after a successful mutation. Does not own `this`. + using AfterHook = std::function; + + LegLabels() = default; + explicit LegLabels(LegLabelList labels); - /// Number of legs (== `labels().size()`). - int64 num_legs = 0; + LegLabels(LegLabels const& other); + LegLabels(LegLabels&& other) noexcept; + LegLabels& operator=(LegLabels const& other); + LegLabels& operator=(LegLabels&& other) noexcept; - explicit LabelledLegs(OptionalLabels labels); - virtual ~LabelledLegs() = default; + /// Assign from a label list (same length required if already non-empty). + LegLabels& operator=(LegLabelList labels); - LabelledLegs(LabelledLegs const&) = default; - LabelledLegs(LabelledLegs&&) = default; - LabelledLegs& operator=(LabelledLegs const&) = default; - LabelledLegs& operator=(LabelledLegs&&) = default; + [[nodiscard]] bool operator==(LegLabelList const& other) const; + [[nodiscard]] bool operator!=(LegLabelList const& other) const { return !(*this == other); } + [[nodiscard]] bool operator==(LegLabels const& other) const; + [[nodiscard]] bool operator!=(LegLabels const& other) const { return !(*this == other); } - /// Perform sanity checks. - virtual void test_sanity() const; + /// Install mutation hooks (non-owning). Cleared on copy/move of the LegLabels value; + /// owners must re-bind after copy. + void set_hooks(BeforeHook before, AfterHook after); + void clear_hooks(); + + void test_sanity() const; - /// Whether every leg has a non-`nullopt` label. [[nodiscard]] bool is_fully_labelled() const; - /// The labels that refer to the legs. - /// - /// Thus, `labels[:K]` are the `codomain_labels` and `labels[K:][::-1]` are the - /// `domain_labels` where `K == num_codomain_legs`. - /// - /// @returns A copy of the internal label list. - [[nodiscard]] OptionalLabels labels() const; - - /// Parse leg indices or leg labels to leg indices (indices of the legs). - [[nodiscard]] std::vector get_leg_idcs(int64 idx) const; - [[nodiscard]] std::vector get_leg_idcs(std::string const& label) const; - [[nodiscard]] std::vector get_leg_idcs( + [[nodiscard]] std::size_t size() const { return labels_.size(); } + [[nodiscard]] int64 num_legs() const { return static_cast(labels_.size()); } + [[nodiscard]] bool empty() const { return labels_.empty(); } + + /// Copy of the internal label list. + [[nodiscard]] LegLabelList as_list() const { return labels_; } + + [[nodiscard]] std::string const& operator[](int64 idx) const; + [[nodiscard]] std::string const& at(int64 idx) const { return (*this)[idx]; } + + /// Proxy enabling `labels[i] = ...` and `labels["old"] = "new"`. + class Ref + { + public: + Ref(LegLabels& self, std::variant key) + : self_(self) + , key_(std::move(key)) + { + } + + Ref& operator=(std::string label); + Ref& operator=(char const* label) { return *this = std::string(label); } + + [[nodiscard]] operator std::string const&() const; + + private: + LegLabels& self_; + std::variant key_; + }; + + Ref operator[](int64 idx); + Ref operator[](std::string const& label); + + /// Set all labels in-place (same length if already sized). + LegLabels& set(LegLabelList labels); + + /// Apply mapping to labels in-place (keys are current labels). + LegLabels& update(std::map const& mapping); + + /// Normalize an int index or look up a label → single leg index. + [[nodiscard]] int64 index(int64 idx) const; + [[nodiscard]] int64 index(std::string const& label) const; + + /// Parse a sequence of indices/labels to leg indices. + [[nodiscard]] std::vector indices( std::vector> const& idcs) const; - /// True if all given labels are present. - [[nodiscard]] bool has_label(std::string const& label) const; - [[nodiscard]] bool has_label(std::vector const& labels) const; + [[nodiscard]] bool has(std::string const& label) const; + [[nodiscard]] bool has(std::vector const& labels) const; + [[nodiscard]] bool contains(std::string const& label) const { return has(label); } - /// If the given labels and `labels()` are the same, up to permutation. + /// If the given labels and this are the same, up to permutation. /// If `planar` is true, only cyclic permutations are accepted. - [[nodiscard]] bool labels_are(std::vector const& labels, - bool planar = false) const; + [[nodiscard]] bool are(std::vector const& labels, bool planar = false) const; - /// Apply mapping to labels. In-place. Returns `*this`. - LabelledLegs& relabel(std::map const& mapping); + void save_hdf5(lasyten::hdf5::Saver& saver, + HighFive::Group& h5gr, + std::string const& subpath) const; - /// Set a single label at given position, in-place. Return the modified instance. - virtual LabelledLegs& set_label(int64 pos, OptionalLabel label); + [[nodiscard]] static LegLabels from_hdf5(lasyten::hdf5::Loader& loader, + HighFive::Group& h5gr, + std::string const& subpath); - /// Set the given labels, in-place. Return the modified instance. - virtual LabelledLegs& set_labels(OptionalLabels labels); + private: + friend class Ref; - /// Label → leg-index map (excludes `nullopt` labels). - [[nodiscard]] std::unordered_map const& labelmap() const - { - return _labelmap; - } + void assign_impl(LegLabelList labels, bool require_same_size); + void set_at(int64 pos, std::string label); + void rename(std::string const& old_label, std::string new_label); + [[nodiscard]] int64 find_label(std::string const& label) const; - protected: - OptionalLabels _labels; - std::unordered_map _labelmap; + LegLabelList labels_; + BeforeHook before_hook_; + AfterHook after_hook_; }; } // namespace lasyten diff --git a/include/lasyten/tensors/mask.h b/include/lasyten/tensors/mask.h index 14257aff..bc352cef 100644 --- a/include/lasyten/tensors/mask.h +++ b/include/lasyten/tensors/mask.h @@ -103,7 +103,7 @@ class Mask : public Tensor bool is_projection, TensorBackend::Ptr backend, Symmetry::Ptr symmetry, - OptionalLabels labels, + LegLabelList labels, std::string device); ~Mask() override = default; @@ -134,7 +134,7 @@ class Mask : public Tensor [[nodiscard]] static Ptr from_eye(Space::Ptr leg, bool is_projection = true, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt); /// Create a projection Mask from a boolean block. @@ -153,7 +153,7 @@ class Mask : public Tensor [[nodiscard]] static Ptr from_block_mask(BlockBackend::BlockPtr block_mask, Space::Ptr large_leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt); /// Create a projection Mask from a boolean DiagonalTensor. @@ -181,7 +181,7 @@ class Mask : public Tensor [[nodiscard]] static Ptr from_indices(py::object indices, Space::Ptr large_leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt); /// Create a random projection Mask. @@ -204,7 +204,7 @@ class Mask : public Tensor TensorBackend::Ptr backend = nullptr, float64 p_keep = 0.5, int64 min_keep = 0, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt, py::object np_random = py::none()); @@ -219,7 +219,7 @@ class Mask : public Tensor /// The projection (or inclusion) Mask that keeps all states [[nodiscard]] static Ptr from_zero(Space::Ptr large_leg, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional device = std::nullopt); /// Import Mask from hdf5 diff --git a/include/lasyten/tensors/planar.h b/include/lasyten/tensors/planar.h index c863f378..a84da33b 100644 --- a/include/lasyten/tensors/planar.h +++ b/include/lasyten/tensors/planar.h @@ -61,9 +61,11 @@ void _assert_cyclic_labels(std::string const& name, /// For each of the legs, an optional symbol to represent its dimension. /// cost_to_make : `BigOPolynomial` /// Algorithmic cost of creating the tensor. -class TensorPlaceholder : public LabelledLegs +class TensorPlaceholder { public: + LegLabels labels; + int64 num_legs = 0; std::vector dims; BigOPolynomial cost_to_make; @@ -549,7 +551,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, std::optional cutoff_singular_values = std::nullopt); /// Checks if two tensors are equal up to numerical tolerance and planar permutation. @@ -687,7 +689,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -715,7 +717,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -733,7 +735,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, std::optional sort = std::nullopt); @@ -779,7 +781,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false); /// Planar version of `partial_trace`. @@ -872,7 +874,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false); /// Planar singular value decomposition (SVD) of a tensor. @@ -932,7 +934,7 @@ class PlanarLinearOperator : public LinearOperator TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, std::optional algorithm = std::nullopt); @@ -941,7 +943,7 @@ class PlanarLinearOperator : public LinearOperator planar_truncated_svd(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels = std::nullopt, + std::optional new_labels = std::nullopt, bool new_leg_dual = false, std::optional algorithm = std::nullopt, std::optional normalize_to = std::nullopt, diff --git a/include/lasyten/tensors/sparse.h b/include/lasyten/tensors/sparse.h index 1f30d165..b31eac67 100644 --- a/include/lasyten/tensors/sparse.h +++ b/include/lasyten/tensors/sparse.h @@ -16,7 +16,7 @@ namespace lasyten { -using VectorLabels = std::optional; +using VectorLabels = std::optional; inline constexpr float64 kGramSchmidtDefaultRcond = 1.0e-14; bool same_legs(std::vector const& legs1, std::vector const& legs2); @@ -27,8 +27,9 @@ bool same_legs(std::vector const& legs1, std::vector const& /// /// vector_legs : list of Space /// The legs of tensors that this operator can act on. -/// vector_labels : list of str or None -/// Labels of the vectors that this operator can act on, or ``None``. +/// vector_labels : list of str, or unset +/// Labels of the vectors that this operator can act on (empty string = unset label), +/// or unset for the whole list. /// dtype : Dtype /// The dtype of a full representation of the operator /// acts_on : list of str diff --git a/include/lasyten/tensors/symmetric_tensor.h b/include/lasyten/tensors/symmetric_tensor.h index d1bc6a22..ba93eec0 100644 --- a/include/lasyten/tensors/symmetric_tensor.h +++ b/include/lasyten/tensors/symmetric_tensor.h @@ -62,7 +62,7 @@ class SymmetricTensor : public Tensor TensorProduct::Ptr domain, TensorBackend::Ptr backend, Symmetry::Ptr symmetry, - OptionalLabels labels, + LegLabelList labels, bool check_complex_dtype = true); ~SymmetricTensor() override = default; @@ -105,7 +105,7 @@ class SymmetricTensor : public Tensor TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -114,7 +114,7 @@ class SymmetricTensor : public Tensor TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt, float64 tol = 1e-6, @@ -126,7 +126,7 @@ class SymmetricTensor : public Tensor Leg::Ptr space, TensorBackend::Ptr backend = nullptr, std::optional device = std::nullopt, - OptionalLabel label = std::nullopt); + std::string label = ""); /// The identity map as a SymmetricTensor. /// @@ -141,20 +141,19 @@ class SymmetricTensor : public Tensor /// backend. [[nodiscard]] static Ptr from_eye(TensorProduct::Ptr co_domain, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Complex128, std::optional device = std::nullopt); /// Generate a sample from the normal distribution. - [[nodiscard]] static Ptr from_random_normal( - TensorProduct::Ptr codomain, - TensorProduct::Ptr domain = nullptr, - TensorCPtr mean = nullptr, - float64 sigma = 1.0, - TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, - std::optional dtype = Dtype::Complex128, - std::optional device = std::nullopt); + [[nodiscard]] static Ptr from_random_normal(TensorProduct::Ptr codomain, + TensorProduct::Ptr domain = nullptr, + TensorCPtr mean = nullptr, + float64 sigma = 1.0, + TensorBackend::Ptr backend = nullptr, + std::optional labels = std::nullopt, + std::optional dtype = Dtype::Complex128, + std::optional device = std::nullopt); /// Generate a tensor with uniformly random block-entries. /// @@ -171,13 +170,12 @@ class SymmetricTensor : public Tensor /// `SymmetricTensor`. /// @param dtype The dtype for the tensor. /// @param device The device of the tensor. If omitted, use the default device of the backend. - [[nodiscard]] static Ptr from_random_uniform( - TensorProduct::Ptr codomain, - TensorProduct::Ptr domain = nullptr, - TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, - Dtype dtype = Dtype::Complex128, - std::optional device = std::nullopt); + [[nodiscard]] static Ptr from_random_uniform(TensorProduct::Ptr codomain, + TensorProduct::Ptr domain = nullptr, + TensorBackend::Ptr backend = nullptr, + std::optional labels = std::nullopt, + Dtype dtype = Dtype::Complex128, + std::optional device = std::nullopt); /// Initialize a `SymmetricTensor` by generating its blocks from a function. /// @@ -211,7 +209,7 @@ class SymmetricTensor : public Tensor TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -220,7 +218,7 @@ class SymmetricTensor : public Tensor TensorProduct::Ptr co_domain, Sector sector, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -244,7 +242,7 @@ class SymmetricTensor : public Tensor TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, std::optional dtype = std::nullopt, std::optional device = std::nullopt); @@ -258,7 +256,7 @@ class SymmetricTensor : public Tensor [[nodiscard]] static Ptr from_zero(TensorProduct::Ptr codomain, TensorProduct::Ptr domain = nullptr, TensorBackend::Ptr backend = nullptr, - std::optional labels = std::nullopt, + std::optional labels = std::nullopt, Dtype dtype = Dtype::Complex128, std::optional device = std::nullopt); diff --git a/include/lasyten/tensors/tensor.h b/include/lasyten/tensors/tensor.h index 6949e7bb..2963a073 100644 --- a/include/lasyten/tensors/tensor.h +++ b/include/lasyten/tensors/tensor.h @@ -60,8 +60,7 @@ namespace lasyten { /// shape: tuple of int /// The dimension of each of the `legs`. class Tensor - : public LabelledLegs - , public VectorLike + : public VectorLike , public std::enable_shared_from_this { public: @@ -74,6 +73,11 @@ class Tensor /// Subclass-overridable forbidden dtypes (e.g. DiagonalTensor allows bool). [[nodiscard]] virtual std::vector const& forbidden_dtypes() const; + /// Leg labels (composition; empty string = unset). + LegLabels labels; + /// Number of legs (== `labels.size()`). Kept in sync with `labels`. + int64 num_legs = 0; + TensorProduct::Ptr codomain; TensorProduct::Ptr domain; TensorBackend::Ptr backend; @@ -88,7 +92,7 @@ class Tensor TensorProduct::Ptr domain, TensorBackend::Ptr backend, Symmetry::Ptr symmetry, - OptionalLabels labels, + LegLabelList labels, Dtype dtype, std::string device); @@ -99,7 +103,7 @@ class Tensor TensorProduct::Ptr domain; TensorBackend::Ptr backend; Symmetry::Ptr symmetry; - OptionalLabels labels; + LegLabelList labels; }; Tensor(InitParsed init, Dtype dtype, std::string device); @@ -128,13 +132,13 @@ class Tensor /// Also supports a special case for input formats of endomorphisms (maps where domain /// and codomain coincide), where a flat list of labels for the codomain can be given, /// and the domain labels are auto-filled with the respective dual labels. - static OptionalLabels _init_parse_labels(std::optional labels, - TensorProduct::Ptr const& codomain, - TensorProduct::Ptr const& domain, - bool is_endomorphism = false); + static LegLabelList _init_parse_labels(std::optional labels, + TensorProduct::Ptr const& codomain, + TensorProduct::Ptr const& domain, + bool is_endomorphism = false); /// Perform sanity checks. - void test_sanity() const override; + virtual void test_sanity() const; /// An ascii representation of the tensor. /// @@ -213,7 +217,7 @@ class Tensor bool understood_braiding = false) = 0; /// The labels that refer to legs in the codomain. - [[nodiscard]] OptionalLabels codomain_labels() const; + [[nodiscard]] LegLabelList codomain_labels() const; /// The hermitian conjugate tensor, a.k.a the dagger of a tensor. /// @@ -246,7 +250,7 @@ class Tensor [[nodiscard]] virtual Ptr dagger() const; /// The labels that refer to legs in the domain. - [[nodiscard]] OptionalLabels domain_labels() const; + [[nodiscard]] LegLabelList domain_labels() const; /// If any of the legs is a pipe [[nodiscard]] bool has_pipes() const; @@ -343,9 +347,6 @@ class Tensor [[nodiscard]] std::vector get_leg_co_domain( std::vector> const& which_legs) const; - /// Set the given labels, in-place. Return the modified instance. - Tensor& set_labels(OptionalLabels labels) override; - /// Convert to a numpy array. [[nodiscard]] py::array to_numpy( std::optional>> leg_order = std::nullopt, @@ -384,10 +385,10 @@ std::tuple Tensor | DirectSum: inv = SymmetricTensor.from_eye( [self.pipe], backend=self.backend, - labels=[None, ChargedTensor._CHARGE_LEG_LABEL], + labels=['', ChargedTensor._CHARGE_LEG_LABEL], dtype=Dtype.from_numpy_dtype(self.dtype), ) tens = ChargedTensor(inv, charged_state=block) @@ -372,7 +372,7 @@ def flat_array_to_tensor(self, vec: np.ndarray) -> Tensor | DirectSum: ) res = self._split_vector_legs(tens) if self.labels is not None: - res.set_labels(self.labels) + res.labels.set(self.labels) return res def tensor_to_flat_array(self, tens: Tensor | DirectSum) -> np.ndarray: @@ -384,12 +384,8 @@ def tensor_to_flat_array(self, tens: Tensor | DirectSum) -> np.ndarray: raise ValueError('DirectSum component count does not match operator') parts = [conv.tensor_to_flat_array(comp) for conv, comp in zip(self.component_converters, tens.components)] return np.concatenate(parts) - if ( - self.labels is not None - and all(l is not None for l in self.labels) - and all(l in tens.labels for l in self.labels) - ): - tens = permute_legs(tens, tens.get_leg_idcs(self.labels)) + if self.labels is not None and all(self.labels) and all(l in tens.labels for l in self.labels): + tens = permute_legs(tens, tens.labels.indices(self.labels)) tens = self._combine_vector_legs(tens) if self._charge_sector is None: res = tens.to_dense_block(understood_braiding=True) diff --git a/lasyten/testing/random_generation.py b/lasyten/testing/random_generation.py index eb21e6b5..7d1c653a 100644 --- a/lasyten/testing/random_generation.py +++ b/lasyten/testing/random_generation.py @@ -367,7 +367,7 @@ def random_tensor( if len(dom_factors) > 1 else spaces.TensorProduct([], symmetry=symmetry) ) - pub_labels = like.labels[:-1] + pub_labels = like.labels.as_list()[:-1] charged = random_tensor( symmetry=symmetry, backend=backend, diff --git a/pybind/symmetries/py_spaces.cpp b/pybind/symmetries/py_spaces.cpp index 18c25015..bc672d87 100644 --- a/pybind/symmetries/py_spaces.cpp +++ b/pybind/symmetries/py_spaces.cpp @@ -704,7 +704,7 @@ bind_elementary_space(py::module_& m) "direct_sum", [](ElementarySpace const& self, py::args others_obj, - std::optional summand_labels) { + std::optional summand_labels) { std::vector others; others.reserve(static_cast(others_obj.size())); for (py::handle item : others_obj) { @@ -768,7 +768,7 @@ bind_direct_sum_space(py::module_& m) cls.def( py::init( - [](py::sequence spaces_obj, bool is_dual, std::optional summand_labels) { + [](py::sequence spaces_obj, bool is_dual, std::optional summand_labels) { std::vector spaces; spaces.reserve(static_cast(spaces_obj.size())); for (py::handle item : spaces_obj) { @@ -788,7 +788,7 @@ bind_direct_sum_space(py::module_& m) cls .def_static( "from_spaces", - [](py::sequence spaces_obj, bool is_dual, std::optional summand_labels) { + [](py::sequence spaces_obj, bool is_dual, std::optional summand_labels) { std::vector spaces; spaces.reserve(static_cast(spaces_obj.size())); for (py::handle item : spaces_obj) { @@ -875,7 +875,7 @@ bind_direct_sum_space(py::module_& m) [](DirectSumSpace const& self, py::object which, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional device) { DirectSumSpace::SummandRef ref = py::isinstance(which) @@ -894,7 +894,7 @@ bind_direct_sum_space(py::module_& m) [](DirectSumSpace const& self, py::object which, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional device) { DirectSumSpace::SummandRef ref = py::isinstance(which) diff --git a/pybind/tensors/py_charged_tensor.cpp b/pybind/tensors/py_charged_tensor.cpp index c4a10151..03082037 100644 --- a/pybind/tensors/py_charged_tensor.cpp +++ b/pybind/tensors/py_charged_tensor.cpp @@ -300,28 +300,6 @@ bind_tensors_charged_tensor(py::module_& m) py::arg("device"), DOC(lasyten, ChargedTensor, move_to_device)); - cls.def( - "set_label", - [](ChargedTensor& self, int64 pos, py::object label) -> ChargedTensor& { - OptionalLabel lab = label.is_none() - ? std::nullopt - : std::optional{ label.cast() }; - self.set_label(pos, lab); - return self; - }, - py::arg("pos"), - py::arg("label"), - DOC(lasyten, ChargedTensor, set_label)); - - cls.def( - "set_labels", - [](ChargedTensor& self, py::object labels) -> ChargedTensor& { - self.set_labels(parse_tensor_init_labels(labels, self.codomain, self.domain)); - return self; - }, - py::arg("labels"), - DOC(lasyten, ChargedTensor, set_labels)); - cls.def("to_backend", &ChargedTensor::to_backend, py::arg("backend"), diff --git a/pybind/tensors/py_constructors.cpp b/pybind/tensors/py_constructors.cpp index 70c937e5..f7aadf94 100644 --- a/pybind/tensors/py_constructors.cpp +++ b/pybind/tensors/py_constructors.cpp @@ -70,7 +70,7 @@ bind_tensors_constructors(py::module_& m) if (!domain.is_none()) { dom = tensor_product_from_python(domain, t->symmetry); } - std::optional labs; + std::optional labs; if (!labels.is_none()) { labs = parse_tensor_init_labels(labels, t->codomain, t->domain); } @@ -118,7 +118,7 @@ matches C++ null / ``nullopt``. ``understood_braiding`` applies only to the bloc std::optional legs_pos_opt; std::optional codomain_pos_opt; std::optional domain_pos_opt; - OptionalLabel label_opt = std::nullopt; + std::string label_opt = ""; if (!legs_pos.is_none()) { legs_pos_opt = legs_pos.cast(); } @@ -158,8 +158,8 @@ matches C++ null / ``nullopt``. ``understood_braiding`` applies only to the bloc [](py::object grid, py::object labels, py::object dtype, - std::optional row_labels, - std::optional col_labels) { + std::optional row_labels, + std::optional col_labels) { std::optional dtype_opt; if (!dtype.is_none()) { dtype_opt = dtype.cast(); @@ -176,7 +176,7 @@ matches C++ null / ``nullopt``. ``understood_braiding`` applies only to the bloc auto res = tensor_from_grid( std::move(g), std::nullopt, dtype_opt, std::move(row_labels), std::move(col_labels)); if (!labels.is_none()) { - res->set_labels(parse_tensor_init_labels(labels, res->codomain, res->domain)); + res->labels.set(parse_tensor_init_labels(labels, res->codomain, res->domain)); } return HiddenLegTensor::maybe_wrap(std::dynamic_pointer_cast(res)); }, diff --git a/pybind/tensors/py_decompositions.cpp b/pybind/tensors/py_decompositions.cpp index ad5a0eca..96347ea1 100644 --- a/pybind/tensors/py_decompositions.cpp +++ b/pybind/tensors/py_decompositions.cpp @@ -19,13 +19,13 @@ namespace lasyten { namespace { -OptionalLabels +LegLabelList py_leg_labels(py::object seq) { - OptionalLabels out; + LegLabelList out; for (auto item : py::reinterpret_borrow(seq)) { if (item.is_none()) { - out.push_back(std::nullopt); + out.push_back(""); } else { out.push_back(item.cast()); } @@ -33,7 +33,7 @@ py_leg_labels(py::object seq) return out; } -std::optional +std::optional py_opt_labels(py::object obj) { if (obj.is_none()) { diff --git a/pybind/tensors/py_diagonal_tensor.cpp b/pybind/tensors/py_diagonal_tensor.cpp index 2e1332d4..3c8e9f5f 100644 --- a/pybind/tensors/py_diagonal_tensor.cpp +++ b/pybind/tensors/py_diagonal_tensor.cpp @@ -324,7 +324,7 @@ bind_tensors_diagonal_tensor(py::module_& m) std::optional device) { auto mean_t = py_optional_tensor(mean); Space::Ptr sp; - std::optional labs; + std::optional labs; if (!leg.is_none()) { auto init = py_parse_diag(leg, std::move(backend), labels); sp = py_as_space_leg(leg); diff --git a/pybind/tensors/py_factory_parse.hpp b/pybind/tensors/py_factory_parse.hpp index b4a7e713..62174081 100644 --- a/pybind/tensors/py_factory_parse.hpp +++ b/pybind/tensors/py_factory_parse.hpp @@ -3,7 +3,7 @@ /// Python-flexible parsing for tensor factory bindings. /// /// Sequence-of-spaces (co)domains, nested labels, numpy blocks, and optional tensors stay at -/// the binding layer. Typed C++ factories receive Ptrs / BlockPtrs / OptionalLabels. +/// the binding layer. Typed C++ factories receive Ptrs / BlockPtrs / LegLabelList. #include #include diff --git a/pybind/tensors/py_helpers.cpp b/pybind/tensors/py_helpers.cpp index a85a22c1..39d412d6 100644 --- a/pybind/tensors/py_helpers.cpp +++ b/pybind/tensors/py_helpers.cpp @@ -16,13 +16,13 @@ namespace lasyten { namespace { -OptionalLabels +LegLabelList py_leg_labels(py::object seq) { - OptionalLabels out; + LegLabelList out; for (auto item : py::reinterpret_borrow(seq)) { if (item.is_none()) { - out.push_back(std::nullopt); + out.push_back(""); } else { out.push_back(item.cast()); } diff --git a/pybind/tensors/py_labels.cpp b/pybind/tensors/py_labels.cpp index f4ef697f..e97681c3 100644 --- a/pybind/tensors/py_labels.cpp +++ b/pybind/tensors/py_labels.cpp @@ -2,10 +2,13 @@ #include "../doc_plus.h" #include "../py_lasyten_pybind11.h" +#include "../tools/hdf5_bind.h" #include "docstrings/tensors/labels.h" +#include #include +#include #include #include @@ -13,23 +16,23 @@ namespace lasyten { namespace { -OptionalLabel +std::string as_leg_label(py::handle obj) { if (obj.is_none()) { - return std::nullopt; + return ""; } return obj.cast(); } std::vector -py_get_leg_idcs(LabelledLegs const& self, py::object idcs) +py_indices(LegLabels const& self, py::object idcs) { if (py::isinstance(idcs)) { - return self.get_leg_idcs(idcs.cast()); + return { self.index(idcs.cast()) }; } if (py::isinstance(idcs)) { - return self.get_leg_idcs(idcs.cast()); + return { self.index(idcs.cast()) }; } std::vector> parsed; for (auto item : py::reinterpret_borrow(to_iterable(idcs))) { @@ -39,7 +42,16 @@ py_get_leg_idcs(LabelledLegs const& self, py::object idcs) parsed.emplace_back(item.cast()); } } - return self.get_leg_idcs(parsed); + return self.indices(parsed); +} + +int64 +py_index(LegLabels const& self, py::object idx) +{ + if (py::isinstance(idx)) { + return self.index(idx.cast()); + } + return self.index(idx.cast()); } std::vector @@ -53,15 +65,15 @@ args_as_strings(py::args const& args) return out; } -std::optional> g_labelled_legs; +std::optional> g_labelled_legs; } // namespace void declare_tensors_labels(py::module_& m) { - g_labelled_legs = py::class_(m, "LabelledLegs"); - (*g_labelled_legs).doc() = DOC(lasyten, LabelledLegs); + g_labelled_legs = py::class_(m, "LegLabels"); + (*g_labelled_legs).doc() = DOC(lasyten, LegLabels); } void @@ -87,7 +99,7 @@ bind_tensors_labels(py::module_& m) if (!py::isinstance(label)) { return false; } - return is_valid_leg_label(OptionalLabel{ label.cast() }); + return is_valid_leg_label(std::string{ label.cast() }); }, py::arg("label"), DOC(lasyten, is_valid_leg_label)); @@ -122,7 +134,7 @@ bind_tensors_labels(py::module_& m) m.def( "_get_matching_labels", - [](OptionalLabels const& labels1, OptionalLabels const& labels2, int64 /*stacklevel*/) { + [](LegLabelList const& labels1, LegLabelList const& labels2, int64 /*stacklevel*/) { return _get_matching_labels(labels1, labels2); }, py::arg("labels1"), @@ -131,82 +143,102 @@ bind_tensors_labels(py::module_& m) doc_plus(DOC(lasyten, _get_matching_labels), R"pydoc( The ``stacklevel`` argument is Python-only (accepted for API compatibility; unused). -In Python, ``None`` labels correspond to C++ ``nullopt``. +In Python, unset labels are empty strings (``""``). )pydoc")); auto& labelled_legs = *g_labelled_legs; - labelled_legs.def(py::init(), py::arg("labels")) - .def_readwrite("num_legs", &LabelledLegs::num_legs) - .def_property_readonly("is_fully_labelled", - &LabelledLegs::is_fully_labelled, - DOC(lasyten, LabelledLegs, is_fully_labelled)) - .def_property( - "labels", - &LabelledLegs::labels, - [](LabelledLegs& self, OptionalLabels labels) { self.set_labels(std::move(labels)); }, - DOC(lasyten, LabelledLegs, labels)) - // Python free functions often access ``_labels``; keep as alias until those are converted. - .def_property( - "_labels", - &LabelledLegs::labels, - [](LabelledLegs& self, OptionalLabels labels) { self.set_labels(std::move(labels)); }) - .def_property_readonly("_labelmap", - [](LabelledLegs const& self) { - // Expose the C++ label→index map to Python (used by free - // functions / tests). - py::dict out; - for (auto const& [lab, idx] : self.labelmap()) { - out[py::cast(lab)] = idx; - } - return out; - }) - .def("test_sanity", &LabelledLegs::test_sanity, DOC(lasyten, LabelledLegs, test_sanity)) - .def("get_leg_idcs", - &py_get_leg_idcs, - py::arg("idcs"), - DOC(lasyten, LabelledLegs, get_leg_idcs)) + labelled_legs.def(py::init(), py::arg("labels")) + .def_property_readonly("num_legs", &LegLabels::num_legs) + .def_property_readonly("is_fully_labelled", &LegLabels::is_fully_labelled) + .def("as_list", &LegLabels::as_list, DOC(lasyten, LegLabels, as_list)) + .def("test_sanity", &LegLabels::test_sanity) .def( - "has_label", - [](LabelledLegs const& self, py::args args) { - if (args.size() == 0) { - throw py::value_error("has_label() requires at least one label"); - } - return self.has_label(args_as_strings(args)); + "set", + [](LegLabels& self, LegLabelList labels) -> LegLabels& { + return self.set(std::move(labels)); }, - DOC(lasyten, LabelledLegs, has_label)) + py::arg("labels"), + py::return_value_policy::reference, + DOC(lasyten, LegLabels, set)) .def( - "labels_are", - [](LabelledLegs const& self, py::args args, bool planar) { - return self.labels_are(args_as_strings(args), planar); + "update", + [](LegLabels& self, std::map const& mapping) -> LegLabels& { + return self.update(mapping); }, - py::kw_only(), - py::arg("planar") = false, - DOC(lasyten, LabelledLegs, labels_are)) - .def( - "relabel", - [](LabelledLegs& self, std::map const& mapping) - -> LabelledLegs& { return self.relabel(mapping); }, py::arg("mapping"), py::return_value_policy::reference, - DOC(lasyten, LabelledLegs, relabel)) + DOC(lasyten, LegLabels, update)) + .def("index", &py_index, py::arg("idx"), DOC(lasyten, LegLabels, index)) + .def("indices", &py_indices, py::arg("idcs"), DOC(lasyten, LegLabels, indices)) + .def("has", + [](LegLabels const& self, py::args args) { + if (args.size() == 0) { + throw py::value_error("has() requires at least one label"); + } + return self.has(args_as_strings(args)); + }) .def( - "set_label", - [](LabelledLegs& self, int64 pos, py::object label) -> LabelledLegs& { - return self.set_label(pos, as_leg_label(label)); + "are", + [](LegLabels const& self, py::args args, bool planar) { + return self.are(args_as_strings(args), planar); }, - py::arg("pos"), - py::arg("label"), - py::return_value_policy::reference, - DOC(lasyten, LabelledLegs, set_label)) + py::kw_only(), + py::arg("planar") = false, + DOC(lasyten, LegLabels, are)) + .def("__len__", &LegLabels::size) + .def("__getitem__", + [](LegLabels const& self, py::object key) -> py::object { + if (py::isinstance(key)) { + return py::cast( + static_cast(self[self.index(key.cast())])); + } + if (py::isinstance(key)) { + return py::cast(static_cast(self[key.cast()])); + } + throw py::type_error("LegLabels indices must be int or str"); + }) + .def("__setitem__", + [](LegLabels& self, py::object key, py::object value) { + std::string lab = as_leg_label(value); + if (py::isinstance(key)) { + self[key.cast()] = std::move(lab); + } else if (py::isinstance(key)) { + self[key.cast()] = std::move(lab); + } else { + throw py::type_error("LegLabels indices must be int or str"); + } + }) + .def("__contains__", + [](LegLabels const& self, py::object key) { + if (!py::isinstance(key)) { + return false; + } + return self.has(key.cast()); + }) .def( - "set_labels", - [](LabelledLegs& self, OptionalLabels labels) -> LabelledLegs& { - return self.set_labels(std::move(labels)); + "__eq__", + [](LegLabels const& self, py::object other) { + if (py::isinstance(other)) { + return self == other.cast(); + } + try { + return self == other.cast(); + } catch (py::cast_error const&) { + return false; + } }, - py::arg("labels"), - py::return_value_policy::reference, - DOC(lasyten, LabelledLegs, set_labels)); + py::is_operator()) + .def("save_hdf5", + lasyten::hdf5::wrap_save_hdf5_const(), + py::arg("hdf5_saver"), + py::arg("h5gr"), + py::arg("subpath")) + .def_static("from_hdf5", + lasyten::hdf5::wrap_from_hdf5(), + py::arg("hdf5_loader"), + py::arg("h5gr"), + py::arg("subpath")); } } // namespace lasyten diff --git a/pybind/tensors/py_ops_legs.cpp b/pybind/tensors/py_ops_legs.cpp index b401f01e..2ec6dd8a 100644 --- a/pybind/tensors/py_ops_legs.cpp +++ b/pybind/tensors/py_ops_legs.cpp @@ -81,9 +81,9 @@ int64 py_leg_idx(TensorCPtr const& tensor, py::object key) { if (py::isinstance(key)) { - return tensor->get_leg_idcs(key.cast())[0]; + return tensor->labels.index(key.cast()); } - return tensor->get_leg_idcs(key.cast())[0]; + return tensor->labels.index(key.cast()); } std::optional diff --git a/pybind/tensors/py_planar.cpp b/pybind/tensors/py_planar.cpp index 042518c7..1ca62edc 100644 --- a/pybind/tensors/py_planar.cpp +++ b/pybind/tensors/py_planar.cpp @@ -87,13 +87,13 @@ eval_result_to_py(PlanarResult const& v) return py_from_tensor_or_scalar(v); } -OptionalLabels +LegLabelList py_leg_labels(py::object seq) { - OptionalLabels out; + LegLabelList out; for (auto item : py::reinterpret_borrow(seq)) { if (item.is_none()) { - out.push_back(std::nullopt); + out.push_back(""); } else { out.push_back(item.cast()); } @@ -101,7 +101,7 @@ py_leg_labels(py::object seq) return out; } -std::optional +std::optional py_opt_labels(py::object obj) { if (obj.is_none()) { @@ -427,8 +427,7 @@ labels_as_strings(py::object labels) void bind_tensors_planar(py::module_& m) { - py::class_ tensor_placeholder( - m, "TensorPlaceholder"); + py::class_ tensor_placeholder(m, "TensorPlaceholder"); tensor_placeholder.doc() = DOC(lasyten, TensorPlaceholder); tensor_placeholder @@ -444,9 +443,23 @@ bind_tensors_planar(py::module_& m) py::arg("labels"), py::arg("dims") = py::none(), py::arg("cost_to_make") = py::none()) + .def_property( + "labels", + py::cpp_function([](TensorPlaceholder& self) -> LegLabels& { return self.labels; }, + py::return_value_policy::reference_internal), + py::cpp_function([](TensorPlaceholder& self, py::object value) { + if (py::isinstance(value)) { + self.labels.set(value.cast().as_list()); + } else { + self.labels.set(value.cast()); + } + self.num_legs = self.labels.num_legs(); + })) + .def_readwrite("num_legs", &TensorPlaceholder::num_legs) .def_readwrite("dims", &TensorPlaceholder::dims) .def_readwrite("cost_to_make", &TensorPlaceholder::cost_to_make) .def("copy", &TensorPlaceholder::copy, py::arg("deep") = true) + .def("string_labels", &TensorPlaceholder::string_labels) .def("__repr__", &TensorPlaceholder::__repr__); py::class_ contraction_tree_node(m, diff --git a/pybind/tensors/py_sparse.cpp b/pybind/tensors/py_sparse.cpp index 6443111f..86f86408 100644 --- a/pybind/tensors/py_sparse.cpp +++ b/pybind/tensors/py_sparse.cpp @@ -18,22 +18,22 @@ namespace lasyten { namespace { -std::optional +std::optional optional_labels(py::object labels) { if (labels.is_none()) { return std::nullopt; } - return labels.cast(); + return labels.cast(); } -std::vector +std::vector to_leg_labels(py::object maybe_labels) { - std::vector out; + std::vector out; for (auto item : maybe_labels) { if (item.is_none()) { - out.push_back(std::nullopt); + out.push_back(""); } else { out.push_back(item.cast()); } @@ -41,13 +41,13 @@ to_leg_labels(py::object maybe_labels) return out; } -std::optional +std::optional optional_vector_labels_from_py(py::object labels) { if (labels.is_none()) { return std::nullopt; } - return OptionalLabels(to_leg_labels(labels)); + return LegLabelList(to_leg_labels(labels)); } class PyLinearOperatorAdapter : public LinearOperator @@ -130,7 +130,7 @@ bind_tensors_sparse(py::module_& m) linear_operator.attr("acts_on") = LinearOperator::acts_on; linear_operator - .def(py::init, Dtype, std::optional>(), + .def(py::init, Dtype, std::optional>(), py::arg("vector_legs") = std::vector{}, py::arg("dtype") = Dtype::Float64, py::arg("vector_labels") = py::none()) diff --git a/pybind/tensors/py_symmetric_tensor.cpp b/pybind/tensors/py_symmetric_tensor.cpp index b8ad2398..df6e5822 100644 --- a/pybind/tensors/py_symmetric_tensor.cpp +++ b/pybind/tensors/py_symmetric_tensor.cpp @@ -169,7 +169,7 @@ bind_tensors_symmetric_tensor(py::module_& m) Leg::Ptr space, TensorBackend::Ptr backend, std::optional device, - OptionalLabel label) { + std::string label) { if (!backend) { backend = get_backend(space->symmetry); } @@ -181,7 +181,7 @@ bind_tensors_symmetric_tensor(py::module_& m) py::arg("space"), py::arg("backend") = nullptr, py::arg("device") = py::none(), - py::arg("label") = py::none(), + py::arg("label") = "", DOC(lasyten, SymmetricTensor, from_dense_block_trivial_sector)); cls.def_static( @@ -219,7 +219,7 @@ bind_tensors_symmetric_tensor(py::module_& m) auto mean_t = py_optional_tensor(mean); TensorProduct::Ptr c; TensorProduct::Ptr d; - std::optional labs; + std::optional labs; if (!codomain.is_none()) { auto init = parse_tensor_init(codomain, domain, std::move(backend), labels); c = init.codomain; diff --git a/pybind/tensors/py_tensor.cpp b/pybind/tensors/py_tensor.cpp index 862f3602..6377e005 100644 --- a/pybind/tensors/py_tensor.cpp +++ b/pybind/tensors/py_tensor.cpp @@ -47,15 +47,14 @@ as_leg_ref(py::handle obj) return obj.cast(); } -std::optional> g_tensor; +std::optional> g_tensor; } // namespace void declare_tensors_tensor(py::module_& m) { - g_tensor = - py::class_(m, "Tensor"); + g_tensor = py::class_(m, "Tensor"); (*g_tensor).doc() = DOC(lasyten, Tensor); } @@ -93,6 +92,17 @@ bind_tensors_tensor(py::module_& m) .def_readwrite("symmetry", &Tensor::symmetry) .def_readwrite("dtype", &Tensor::dtype) .def_readwrite("device", &Tensor::device) + .def_readwrite("num_legs", &Tensor::num_legs) + .def_property("labels", + py::cpp_function([](Tensor& self) -> LegLabels& { return self.labels; }, + py::return_value_policy::reference_internal), + py::cpp_function([](Tensor& self, py::object value) { + if (py::isinstance(value)) { + self.labels.set(value.cast().as_list()); + } else { + self.labels.set(value.cast()); + } + })) .def_property( "shape", [](Tensor const& self) { @@ -402,24 +412,15 @@ bind_tensors_tensor(py::module_& m) py::arg("which_leg"), DOC(lasyten, Tensor, get_leg_co_domain)); - cls - .def( - "set_labels", - [](Tensor& self, py::object labels) -> Tensor& { - return self.set_labels(parse_tensor_init_labels(labels, self.codomain, self.domain)); - }, - py::arg("labels"), - py::return_value_policy::reference, - DOC(lasyten, Tensor, set_labels)) - .def( - "to_numpy", - [](Tensor& self, py::object leg_order, py::object numpy_dtype, bool understood_braiding) { - return self.to_numpy(optional_leg_order(leg_order), numpy_dtype, understood_braiding); - }, - py::arg("leg_order") = py::none(), - py::arg("numpy_dtype") = py::none(), - py::arg("understood_braiding") = false, - DOC(lasyten, Tensor, to_numpy)); + cls.def( + "to_numpy", + [](Tensor& self, py::object leg_order, py::object numpy_dtype, bool understood_braiding) { + return self.to_numpy(optional_leg_order(leg_order), numpy_dtype, understood_braiding); + }, + py::arg("leg_order") = py::none(), + py::arg("numpy_dtype") = py::none(), + py::arg("understood_braiding") = false, + DOC(lasyten, Tensor, to_numpy)); } } // namespace lasyten diff --git a/pybind/tensors/py_trampolines.hpp b/pybind/tensors/py_trampolines.hpp index ab3dd4b6..e2b8db46 100644 --- a/pybind/tensors/py_trampolines.hpp +++ b/pybind/tensors/py_trampolines.hpp @@ -93,11 +93,6 @@ class PyTensor { PYBIND11_OVERRIDE(std::vector const&, Tensor, forbidden_dtypes); } - - Tensor& set_labels(OptionalLabels labels) override - { - PYBIND11_OVERRIDE(Tensor&, Tensor, set_labels, labels); - } }; /// pybind11 trampoline for :class:`SymmetricTensor` (Python subclasses e.g. DiagonalTensor). diff --git a/src/models/couplings.cpp b/src/models/couplings.cpp index e54b7a76..fc38a355 100644 --- a/src/models/couplings.cpp +++ b/src/models/couplings.cpp @@ -84,12 +84,12 @@ levels_from_label_dict(SymmetricTensorPtr const& tensor, std::map const& level_dict) { LevelsSpec spec(static_cast(tensor->num_legs), std::nullopt); - auto labels = tensor->labels(); + auto labels = tensor->labels.as_list(); for (std::size_t i = 0; i < labels.size(); ++i) { - if (!labels[i].has_value()) { + if (labels[i].empty()) { continue; } - auto it = level_dict.find(*labels[i]); + auto it = level_dict.find(labels[i]); if (it != level_dict.end()) { spec[i] = it->second; } @@ -153,7 +153,7 @@ swap_adjacent_factors(SymmetricTensorPtr const& Wa, SymmetricTensorPtr const& Wb std::vector{ "wR" }, std::nullopt, false)); - Wa_prepped->relabel({ { "p", "p0" }, { "p*", "p0*" } }); + Wa_prepped->labels.update({ { "p", "p0" }, { "p*", "p0*" } }); SymmetricTensorPtr Wb_prepped = as_symmetric(permute_legs(Wb, std::vector{ "wL" }, @@ -178,11 +178,11 @@ swap_adjacent_factors(SymmetricTensorPtr const& Wa, SymmetricTensorPtr const& Wb true)); auto [left, right] = - horizontal_factorization(T, 2, 1, OptionalLabels{ "wR", "wL" }, std::nullopt); + horizontal_factorization(T, 2, 1, LegLabelList{ "wR", "wL" }, std::nullopt); SymmetricTensorPtr Wleft = as_symmetric(std::move(left)); SymmetricTensorPtr Wright = as_symmetric(std::move(right)); - Wleft->relabel({ { "p1", "p" }, { "p1*", "p*" } }); - Wright->relabel({ { "p0", "p" }, { "p0*", "p*" } }); + Wleft->labels.update({ { "p1", "p" }, { "p1*", "p*" } }); + Wright->labels.update({ { "p0", "p" }, { "p0*", "p*" } }); return { Wleft, Wright }; } @@ -304,8 +304,8 @@ structural_key(Coupling const& coupling) shape_list.append(d); } py::list labels_list; - for (auto const& lab : t->labels()) { - labels_list.append(lab.has_value() ? py::object(py::str(*lab)) : py::none()); + for (auto const& lab : t->labels.as_list()) { + labels_list.append(py::str(lab)); } py::list codomain_spaces; for (auto const& f : t->codomain->factors) { @@ -436,9 +436,9 @@ Coupling::test_sanity() const throw std::runtime_error("Coupling tensor must have 2 codomain and 2 domain legs"); } static std::vector const expected_labels = { "wL", "p", "wR", "p*" }; - auto labels = W->labels(); + auto labels = W->labels.as_list(); for (std::size_t j = 0; j < expected_labels.size(); ++j) { - if (!labels[j].has_value() || *labels[j] != expected_labels[j]) { + if (labels[j] != expected_labels[j]) { throw std::runtime_error("Coupling tensor labels must be ['wL', 'p', 'wR', 'p*']"); } } @@ -489,7 +489,7 @@ Coupling::from_dense_block(py::object operator_, for (std::size_t i = 0; i < sites.size(); ++i) { p_labels.push_back(std::format("p{}", i)); } - OptionalLabels labels; + LegLabelList labels; for (auto const& pl : p_labels) { labels.emplace_back(pl); } @@ -537,14 +537,14 @@ Coupling::from_tensor(SymmetricTensorPtr operator_, for (std::size_t i = 0; i < sites.size(); ++i) { p_labels.push_back(std::format("p{}", i)); } - OptionalLabels expected_labels; + LegLabelList expected_labels; for (auto const& pl : p_labels) { expected_labels.emplace_back(pl); } for (auto it = p_labels.rbegin(); it != p_labels.rend(); ++it) { expected_labels.emplace_back(*it + "*"); } - if (operator_->labels() != expected_labels) { + if (operator_->labels.as_list() != expected_labels) { throw std::invalid_argument("operator labels do not match expected p-label order"); } @@ -562,32 +562,33 @@ Coupling::from_tensor(SymmetricTensorPtr operator_, SymmetricTensorPtr W = as_symmetric(add_trivial_leg(operator_, std::nullopt, 0, std::nullopt, "wL")); W = as_symmetric(add_trivial_leg(W, std::nullopt, std::nullopt, 1, "wR")); - W->relabel({ { "p0", "p" }, { "p0*", "p*" } }); + W->labels.update({ { "p0", "p" }, { "p0*", "p*" } }); factorization = { W }; } else { SymmetricTensorPtr rest = operator_; auto [W0, rest0] = - horizontal_factorization(rest, 1, 1, OptionalLabels{ "wR", "wL" }, cutoff); + horizontal_factorization(rest, 1, 1, LegLabelList{ "wR", "wL" }, cutoff); W0 = as_symmetric(W0); rest = as_symmetric(rest0); - W0->relabel({ { "p0", "p" }, { "p0*", "p*" } }); + W0->labels.update({ { "p0", "p" }, { "p0*", "p*" } }); factorization.push_back( as_symmetric(add_trivial_leg(W0, std::nullopt, 0, std::nullopt, "wL"))); for (std::size_t i = 1; i + 1 < sites.size(); ++i) { auto [Wi, resti] = - horizontal_factorization(rest, 2, 1, OptionalLabels{ "wR", "wL" }, cutoff); + horizontal_factorization(rest, 2, 1, LegLabelList{ "wR", "wL" }, cutoff); Wi = as_symmetric(Wi); rest = as_symmetric(resti); - Wi->relabel({ { std::format("p{}", i), "p" }, { std::format("p{}*", i), "p*" } }); + Wi->labels.update( + { { std::format("p{}", i), "p" }, { std::format("p{}*", i), "p*" } }); factorization.push_back(as_symmetric(Wi)); } if (rest->num_codomain_legs() != 2 || rest->num_domain_legs() != 1) { throw std::runtime_error("Unexpected remaining legs after horizontal factorization"); } - rest->relabel({ { std::format("p{}", sites.size() - 1), "p" }, - { std::format("p{}*", sites.size() - 1), "p*" } }); + rest->labels.update({ { std::format("p{}", sites.size() - 1), "p" }, + { std::format("p{}*", sites.size() - 1), "p*" } }); factorization.push_back( as_symmetric(add_trivial_leg(rest, std::nullopt, std::nullopt, 1, "wR"))); } @@ -604,7 +605,7 @@ Coupling::to_tensor() const std::vector{ LegRef{ int64(1) } }, std::nullopt, false)); - res->relabel({ { "p", "p0" }, { "p*", "p0*" } }); + res->labels.update({ { "p", "p0" }, { "p*", "p0*" } }); int64 const num_sites = static_cast(sites.size()); for (int64 i = 1; i < num_sites; ++i) { @@ -1301,7 +1302,7 @@ sector_projection_coupling(std::vector sites, legs.push_back(as_leg(s->leg)); } auto codomain = std::make_shared(legs); - OptionalLabels labels; + LegLabelList labels; for (std::size_t i = 0; i < sites.size(); ++i) { labels.emplace_back(std::format("p{}", i)); } diff --git a/src/models/degrees_of_freedom.cpp b/src/models/degrees_of_freedom.cpp index 28bfce0a..53390670 100644 --- a/src/models/degrees_of_freedom.cpp +++ b/src/models/degrees_of_freedom.cpp @@ -87,14 +87,14 @@ product_of_legs(std::vector factors) return std::make_shared(std::move(factors)); } -OptionalLabels +LegLabelList pp_labels() { - return OptionalLabels{ "p", "p*" }; + return LegLabelList{ "p", "p*" }; } bool -leg_labels_are_p_pstar(OptionalLabels const& labels) +leg_labels_are_p_pstar(LegLabelList const& labels) { return labels.size() == 2 && labels[0] == "p" && labels[1] == "p*"; } @@ -331,7 +331,7 @@ Site::test_sanity() if (!single_leg_matches(leg, op->codomain) || !single_leg_matches(leg, op->domain)) { throw std::invalid_argument("onsite operator legs must match the site leg"); } - if (!leg_labels_are_p_pstar(op->labels())) { + if (!leg_labels_are_p_pstar(op->labels.as_list())) { throw std::invalid_argument("onsite operator labels must be ('p', 'p*')"); } op->test_sanity(); @@ -373,10 +373,10 @@ Site::add_onsite_operator(std::string const& name, !single_leg_matches(leg, tensor_op->domain)) { throw std::invalid_argument("operator legs must match the site leg"); } - if (!leg_labels_are_p_pstar(tensor_op->labels())) { + if (!leg_labels_are_p_pstar(tensor_op->labels.as_list())) { tensor_op = std::dynamic_pointer_cast(tensor_op->copy(/*deep=*/false)); - tensor_op->set_labels(pp_labels()); + tensor_op->labels.set(pp_labels()); } } else if (is_diagonal.has_value() && *is_diagonal) { py::object block = op.attr("copy")(); @@ -486,13 +486,13 @@ SymmetricTensorPtr Site::identity_tensor(ElementarySpace::Ptr w, bool overbraid) { auto co_domain = product_of_legs(std::vector{ leg, std::move(w) }); - auto tensor = SymmetricTensor::from_eye(co_domain, backend, OptionalLabels{ "p", "w" }); + auto tensor = SymmetricTensor::from_eye(co_domain, backend, LegLabelList{ "p", "w" }); auto permuted = permute_legs(tensor, std::vector{ "w", "p" }, std::vector{ "p*", "w*" }, std::nullopt, !overbraid); - permuted->relabel({ { "w", "wL" }, { "w*", "wR" } }); + permuted->labels.update({ { "w", "wL" }, { "w*", "wR" } }); return std::dynamic_pointer_cast(permuted); } @@ -505,14 +505,14 @@ Site::get_onsite_W_block(std::string const& opname, { auto op = std::const_pointer_cast(TensorCPtr{ get_op(opname) })->as_SymmetricTensor(); auto id_w = SymmetricTensor::from_eye( - product_of_legs(std::vector{ std::move(w) }), backend, OptionalLabels{ "w" }); + product_of_legs(std::vector{ std::move(w) }), backend, LegLabelList{ "w" }); auto tensor = outer(op, id_w); auto permuted = permute_legs(tensor, std::vector{ "w", "p" }, std::vector{ "p*", "w*" }, std::nullopt, !overbraid); - permuted->relabel({ { "w", wL_label }, { "w*", wR_label } }); + permuted->labels.update({ { "w", wL_label }, { "w*", wR_label } }); return std::dynamic_pointer_cast(permuted); } diff --git a/src/models/sites.cpp b/src/models/sites.cpp index 4492e253..e354b9f0 100644 --- a/src/models/sites.cpp +++ b/src/models/sites.cpp @@ -226,7 +226,7 @@ add_anyon_projectors(AnyonDOF& site, SymmetricTensor::from_sector_projection(leg_tp, decomposition[i], backend, - std::optional{ { "p", "p*" } }, + std::optional{ { "p", "p*" } }, std::nullopt, default_device); site.onsite_operators["P_" + names[i]] = std::move(op); diff --git a/src/symmetries/spaces.cpp b/src/symmetries/spaces.cpp index 2644aaf2..5037ff10 100644 --- a/src/symmetries/spaces.cpp +++ b/src/symmetries/spaces.cpp @@ -1631,7 +1631,7 @@ ElementarySpace::change_symmetry(Symmetry::Ptr symmetry, SectorMapFn sector_map, ElementarySpace::Ptr ElementarySpace::direct_sum(std::vector const& others, - std::optional summand_labels) const + std::optional summand_labels) const { std::vector all; all.reserve(1 + others.size()); @@ -1937,12 +1937,12 @@ dss_cumsum_with_leading_zero(std::vector const& mults) return out; } -[[nodiscard]] std::pair, OptionalLabels> +[[nodiscard]] std::pair, LegLabelList> flatten_direct_sum_spaces(std::vector const& spaces, - OptionalLabels const& labels) + LegLabelList const& labels) { std::vector flat; - OptionalLabels flat_labels; + LegLabelList flat_labels; flat.reserve(spaces.size()); flat_labels.reserve(spaces.size()); for (std::size_t i = 0; i < spaces.size(); ++i) { @@ -1956,7 +1956,7 @@ flatten_direct_sum_spaces(std::vector const& spaces, "DirectSumSpace summands must be plain ElementarySpaces (not pipes)"); } if (auto dss = std::dynamic_pointer_cast(s)) { - if (label) { + if (!label.empty()) { throw std::invalid_argument( "Can not attach an explicit summand label to a nested DirectSumSpace summand; " "its own summand_labels are propagated to the flattened slots instead."); @@ -1975,18 +1975,18 @@ flatten_direct_sum_spaces(std::vector const& spaces, /// Duplicate entries (ignoring unset labels) and invalid-syntax labels raise. void -validate_summand_labels(OptionalLabels const& labels) +validate_summand_labels(LegLabelList const& labels) { std::unordered_set seen; for (auto const& l : labels) { - if (!l) { + if (l.empty()) { continue; } if (!is_valid_leg_label(l)) { - throw std::invalid_argument(std::format("Invalid summand label: {}", *l)); + throw std::invalid_argument(std::format("Invalid summand label: {}", l)); } - if (!seen.insert(*l).second) { - throw std::invalid_argument(std::format("Duplicate summand label: {}", *l)); + if (!seen.insert(l).second) { + throw std::invalid_argument(std::format("Duplicate summand label: {}", l)); } } } @@ -1996,10 +1996,9 @@ validate_summand_labels(OptionalLabels const& labels) DirectSumSpace::Prepared DirectSumSpace::prepare(std::vector spaces_in, bool is_dual_, - std::optional summand_labels_in) + std::optional summand_labels_in) { - OptionalLabels labels_in = - summand_labels_in.value_or(OptionalLabels(spaces_in.size(), std::nullopt)); + LegLabelList labels_in = summand_labels_in.value_or(LegLabelList(spaces_in.size(), "")); if (labels_in.size() != spaces_in.size()) { throw std::invalid_argument("summand_labels must have the same length as spaces"); } @@ -2065,15 +2064,15 @@ DirectSumSpace::DirectSumSpace(Prepared prepared, bool is_dual_) , summand_labels(std::move(prepared.summand_labels)) { for (std::size_t i = 0; i < summand_labels.size(); ++i) { - if (summand_labels[i]) { - _summand_labelmap[*summand_labels[i]] = static_cast(i); + if (!summand_labels[i].empty()) { + _summand_labelmap[summand_labels[i]] = static_cast(i); } } } DirectSumSpace::DirectSumSpace(std::vector spaces_, bool is_dual_, - std::optional summand_labels_) + std::optional summand_labels_) : DirectSumSpace(prepare(std::move(spaces_), is_dual_, std::move(summand_labels_)), is_dual_) { } @@ -2081,7 +2080,7 @@ DirectSumSpace::DirectSumSpace(std::vector spaces_, DirectSumSpace::Ptr DirectSumSpace::from_spaces(std::vector spaces_, bool is_dual_, - std::optional summand_labels_) + std::optional summand_labels_) { return std::make_shared( std::move(spaces_), is_dual_, std::move(summand_labels_)); @@ -2099,7 +2098,7 @@ DirectSumSpace::get_summand_idx(SummandRef which) const if (i > 0) { available += ", "; } - available += summand_labels[i] ? *summand_labels[i] : std::string("None"); + available += summand_labels[i].empty() ? std::string("None") : summand_labels[i]; } throw std::invalid_argument( std::format("No summand with label {}. Labels are [{}]", label, available)); @@ -2409,7 +2408,7 @@ DirectSumSpace::save_hdf5(lasyten::hdf5::Saver& saver, hdf5_export::save_elementary_space(spaces_saver, std::to_string(i), spaces[i]); } hdf5_io::h5_set_attr(h5gr.getId(), "is_direct_sum_space", true); - hdf5_export::save_optional_labels(saver, subpath + "summand_labels", summand_labels); + hdf5_export::save_leg_labels(saver, subpath + "summand_labels", summand_labels); } DirectSumSpace::Ptr @@ -2428,12 +2427,11 @@ DirectSumSpace::from_hdf5(lasyten::hdf5::Loader& loader, } auto const is_dual_attr = hdf5_io::h5_get_attr_int64(h5gr.getId(), "is_dual"); bool const is_dual = is_dual_attr.value_or(0) != 0; - std::optional summand_labels_; + std::optional summand_labels_; if (hdf5_io::h5_contains(loader.root(), subpath + "summand_labels")) { - auto labs = hdf5_export::load_optional_labels(loader, subpath + "summand_labels"); - summand_labels_ = labs.empty() && !spaces_.empty() - ? OptionalLabels(spaces_.size(), std::nullopt) - : std::move(labs); + auto labs = hdf5_export::load_leg_labels(loader, subpath + "summand_labels"); + summand_labels_ = + labs.empty() && !spaces_.empty() ? LegLabelList(spaces_.size(), "") : std::move(labs); } auto obj = from_spaces(std::move(spaces_), is_dual, std::move(summand_labels_)); loader.memorize_load(h5gr.getId(), std::static_pointer_cast(obj)); diff --git a/src/tensors/charged_tensor.cpp b/src/tensors/charged_tensor.cpp index 1ffef6c1..9ce11df4 100644 --- a/src/tensors/charged_tensor.cpp +++ b/src/tensors/charged_tensor.cpp @@ -32,12 +32,12 @@ ChargedTensor::ChargedTensor(SymmetricTensor::Ptr inv, BlockBackend::BlockPtr ch inv->backend, inv->symmetry, [&]() { - auto labs = inv->labels(); + auto labs = inv->labels.as_list(); if (labs.empty()) { throw std::invalid_argument( "ChargedTensor invariant_part must have a charge-leg label"); } - return OptionalLabels(labs.begin(), labs.end() - 1); + return LegLabelList(labs.begin(), labs.end() - 1); }(), inv->dtype, inv->device) @@ -50,8 +50,8 @@ ChargedTensor::ChargedTensor(SymmetricTensor::Ptr inv, BlockBackend::BlockPtr ch throw std::invalid_argument( "ChargedTensor invariant_part must have a charge leg in the domain"); } - auto labs = invariant_part->labels(); - if (labs.empty() || !labs.back() || *labs.back() != _CHARGE_LEG_LABEL) { + auto labs = invariant_part->labels.as_list(); + if (labs.empty() || labs.back() != _CHARGE_LEG_LABEL) { throw std::invalid_argument( std::format("ChargedTensor invariant_part last label must be '{}'", _CHARGE_LEG_LABEL)); } @@ -71,15 +71,26 @@ ChargedTensor::ChargedTensor(SymmetricTensor::Ptr inv, BlockBackend::BlockPtr ch charged_state = invariant_part->backend->block_backend->as_block( py::cast(charged_state), invariant_part->dtype, invariant_part->device); invariant_part->allow_charge_leg_label = true; - reject_exclamation_in_labels(labels(), "ChargedTensor"); + reject_exclamation_in_labels(labels.as_list(), "ChargedTensor"); + + labels.set_hooks( + [this](LegLabels& /*self*/, LegLabelList const& proposed) { + reject_exclamation_in_labels(proposed, "ChargedTensor"); + }, + [this](LegLabels& self) { + auto inv_labs = self.as_list(); + inv_labs.emplace_back(_CHARGE_LEG_LABEL); + invariant_part->allow_charge_leg_label = true; + invariant_part->labels.set(std::move(inv_labs)); + }); } void ChargedTensor::test_sanity() const { Tensor::test_sanity(); - auto inv_labs = invariant_part->labels(); - assert(labels() == OptionalLabels(inv_labs.begin(), inv_labs.end() - 1)); + auto inv_labs = invariant_part->labels.as_list(); + assert(labels.as_list() == LegLabelList(inv_labs.begin(), inv_labs.end() - 1)); invariant_part->allow_charge_leg_label = true; invariant_part->test_sanity(); assert(invariant_part->device == device); @@ -89,7 +100,7 @@ ChargedTensor::test_sanity() const std::vector{ static_cast(charge_leg->dim) }, std::nullopt, device); - reject_exclamation_in_labels(labels(), "ChargedTensor"); + reject_exclamation_in_labels(labels.as_list(), "ChargedTensor"); } std::string @@ -129,8 +140,8 @@ ChargedTensor::_parse_inv_domain(TensorProduct::Ptr domain, return { domain->left_multiply(std::dynamic_pointer_cast(charge_leg_sp)), charge_leg_sp }; } -std::tuple -ChargedTensor::_parse_inv_labels(std::optional labels, +std::tuple +ChargedTensor::_parse_inv_labels(std::optional labels, TensorProduct::Ptr const& codomain, TensorProduct::Ptr const& domain) { @@ -147,7 +158,7 @@ ChargedTensor::from_block_func(BlockFactoryFn func, TensorProduct::Ptr domain, BlockBackend::BlockPtr charged_state, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -187,7 +198,7 @@ ChargedTensor::from_dense_block(BlockBackend::BlockPtr block, TensorProduct::Ptr domain, std::optional> charge, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device, float64 tol, @@ -250,7 +261,7 @@ ChargedTensor::from_dense_block_single_sector(BlockBackend::BlockPtr vector, std::make_shared(std::vector{ space }, space->symmetry); auto inv_domain = std::make_shared(std::vector{ charge_leg }, space->symmetry); - OptionalLabels inv_labels{ label, std::string(_CHARGE_LEG_LABEL) }; + LegLabelList inv_labels{ label.value_or(""), std::string(_CHARGE_LEG_LABEL) }; auto inv_part = std::make_shared( data, codomain, inv_domain, backend, space->symmetry, std::move(inv_labels)); auto charged_state = @@ -289,12 +300,12 @@ ChargedTensor::from_two_charge_legs(SymmetricTensor::Ptr invariant_part, BlockBackend::BlockPtr state2) { // Uses combine_legs free function. - auto labs = invariant_part->labels(); + auto labs = invariant_part->labels.as_list(); if (labs.size() < 2) { throw std::invalid_argument("from_two_charge_legs requires at least two labels"); } - if (!labs[labs.size() - 1] || !labs[labs.size() - 1]->starts_with(_CHARGE_LEG_LABEL) || - !labs[labs.size() - 2] || !labs[labs.size() - 2]->starts_with(_CHARGE_LEG_LABEL)) { + if (!labs[labs.size() - 1].starts_with(_CHARGE_LEG_LABEL) || + !labs[labs.size() - 2].starts_with(_CHARGE_LEG_LABEL)) { throw std::invalid_argument( std::format("from_two_charge_legs requires the last two labels to start with '{}'", _CHARGE_LEG_LABEL)); @@ -310,7 +321,7 @@ ChargedTensor::from_two_charge_legs(SymmetricTensor::Ptr invariant_part, throw std::runtime_error( "from_two_charge_legs: expected SymmetricTensor after combine_legs"); } - inv_sym->set_label(-1, _CHARGE_LEG_LABEL); + inv_sym->labels[-1] = _CHARGE_LEG_LABEL; auto pipe = std::dynamic_pointer_cast(inv_sym->domain->factors[0]); if (!pipe) { throw std::runtime_error("from_two_charge_legs: expected LegPipe charge leg"); @@ -326,7 +337,7 @@ ChargedTensor::from_zero(TensorProduct::Ptr codomain, std::variant charge, BlockBackend::BlockPtr charged_state, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -392,7 +403,7 @@ ChargedTensor::as_SymmetricTensor(bool /*guarantee_copy*/, std::optionallabels(); - OptionalLabels dual_rev; + auto labs = invariant_part->labels.as_list(); + LegLabelList dual_rev; dual_rev.reserve(labs.size()); for (auto it = labs.rbegin(); it != labs.rend(); ++it) { dual_rev.push_back(_dual_leg_label(*it)); @@ -450,7 +461,7 @@ ChargedTensor::dagger() const backend, symmetry, std::move(dual_rev)); - inv_sym->set_label(0, _CHARGE_LEG_LABEL); + inv_sym->labels[0] = _CHARGE_LEG_LABEL; auto moved = move_leg(inv_sym, 0, std::nullopt, 0, std::nullopt, BendRight{ true }); auto moved_sym = std::dynamic_pointer_cast(moved); if (!moved_sym) { @@ -509,30 +520,6 @@ ChargedTensor::_repr_header_lines(std::string const& indent, bool use_symm_str) return lines; } -LabelledLegs& -ChargedTensor::set_label(int64 pos, OptionalLabel label) -{ - pos = to_valid_idx(pos, num_legs); - if (label_contains_exclamation(label)) { - throw std::invalid_argument( - "ChargedTensor public labels must not contain '!'. Use HiddenLegTensor to hide legs."); - } - invariant_part->set_label(pos, label); - return LabelledLegs::set_label(pos, label); -} - -Tensor& -ChargedTensor::set_labels(OptionalLabels labels_in) -{ - reject_exclamation_in_labels(labels_in, "ChargedTensor"); - Tensor::set_labels(labels_in); - auto inv_labs = labels(); - inv_labs.emplace_back(std::string(_CHARGE_LEG_LABEL)); - invariant_part->allow_charge_leg_label = true; - invariant_part->set_labels(std::move(inv_labs)); - return *this; -} - Tensor::Ptr ChargedTensor::to_backend(TensorBackend::Ptr new_backend, std::optional dtype_opt, @@ -566,7 +553,7 @@ ChargedTensor::to_dense_block( block = backend->block_backend->to_dtype(block, *dtype_opt); } if (leg_order.has_value()) { - block = backend->block_backend->permute_axes(block, get_leg_idcs(*leg_order)); + block = backend->block_backend->permute_axes(block, labels.indices(*leg_order)); } return block; } @@ -606,7 +593,7 @@ ChargedTensor::save_hdf5(lasyten::hdf5::Saver& saver, hdf5_io::h5_set_attr(h5gr.getId(), "dtype", dtype::repr(dtype)); hdf5_io::h5_set_attr(h5gr.getId(), "num_legs", static_cast(num_legs)); hdf5_export::save_f64_vector(saver, subpath + "shape", shape); - hdf5_export::save_optional_labels(saver, subpath + "labels", _labels); + hdf5_export::save_leg_labels(saver, subpath + "labels", labels.as_list()); } ChargedTensor::Ptr diff --git a/src/tensors/constructors.cpp b/src/tensors/constructors.cpp index cc02922d..339bb1ca 100644 --- a/src/tensors/constructors.cpp +++ b/src/tensors/constructors.cpp @@ -69,7 +69,7 @@ factor_slices_equal(std::vector const& a, std::vector const& TensorPtr eye(Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device, bool diagonal) @@ -87,7 +87,7 @@ tensor(TensorCPtr obj, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -101,9 +101,9 @@ tensor(TensorCPtr obj, throw std::invalid_argument("Mismatching backend"); } TensorPtr out; - if (labels.has_value() && *labels != obj->labels()) { + if (labels.has_value() && *labels != obj->labels.as_list()) { out = std::const_pointer_cast(obj)->copy(); - out->set_labels(*labels); + out->labels.set(*labels); } else { out = std::const_pointer_cast(obj); } @@ -121,7 +121,7 @@ tensor(BlockBackend::BlockPtr obj, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device, bool understood_braiding) @@ -142,11 +142,11 @@ add_trivial_leg(TensorCPtr tens, std::optional legs_pos_opt, std::optional codomain_pos_opt, std::optional domain_pos_opt, - OptionalLabel label, + std::string label, bool is_dual) { - if (label && tens->has_label(*label)) { - throw std::invalid_argument(std::format("Tensor already has label '{}'", *label)); + if (!label.empty() && tens->labels.has(label)) { + throw std::invalid_argument(std::format("Tensor already has label '{}'", label)); } // --- hints from Python add_trivial_leg --- @@ -250,8 +250,8 @@ add_trivial_leg(TensorCPtr tens, auto data = backend->add_trivial_leg(tens, legs_pos, add_to_domain, co_domain_pos, codomain, domain); - OptionalLabels labels = tens->labels(); - OptionalLabels new_labels; + LegLabelList labels = tens->labels.as_list(); + LegLabelList new_labels; new_labels.reserve(labels.size() + 1); new_labels.insert(new_labels.end(), labels.begin(), labels.begin() + legs_pos); new_labels.push_back(label); @@ -270,11 +270,12 @@ TensorPtr zero_like(TensorCPtr tensor) { if (auto mask = std::dynamic_pointer_cast(tensor)) { - return Mask::from_zero(mask->large_leg(), mask->backend, mask->labels(), mask->device); + return Mask::from_zero( + mask->large_leg(), mask->backend, mask->labels.as_list(), mask->device); } if (auto diag = std::dynamic_pointer_cast(tensor)) { return DiagonalTensor::from_zero( - diag->leg(), diag->backend, diag->labels(), diag->dtype, diag->device); + diag->leg(), diag->backend, diag->labels.as_list(), diag->dtype, diag->device); } if (auto charged = std::dynamic_pointer_cast(tensor)) { auto charge = std::dynamic_pointer_cast(charged->charge_leg); @@ -286,24 +287,28 @@ zero_like(TensorCPtr tensor) charge, charged->charged_state, charged->backend, - charged->labels(), + charged->labels.as_list(), charged->dtype, charged->device); } if (auto sym = std::dynamic_pointer_cast(tensor)) { // a HiddenLegTensor keeps the hidden prefix in its labels -> wrap the result again - return HiddenLegTensor::maybe_wrap(SymmetricTensor::from_zero( - sym->codomain, sym->domain, sym->backend, sym->labels(), sym->dtype, sym->device)); + return HiddenLegTensor::maybe_wrap(SymmetricTensor::from_zero(sym->codomain, + sym->domain, + sym->backend, + sym->labels.as_list(), + sym->dtype, + sym->device)); } throw py::type_error("Invalid type for tensor."); } TensorPtr tensor_from_grid(std::vector> grid, - std::optional labels, + std::optional labels, std::optional dtype_opt, - std::optional row_labels, - std::optional col_labels) + std::optional row_labels, + std::optional col_labels) { // --- hints from Python tensor_from_grid --- // check input @@ -496,7 +501,7 @@ tensor_from_grid(std::vector> grid, std::move(right_mult_slices), dtype, device); - OptionalLabels labs = labels.value_or(OptionalLabels{}); + LegLabelList labs = labels.value_or(LegLabelList{}); if (!labels.has_value()) { labs = Tensor::_init_parse_labels(std::nullopt, codomain, domain); } @@ -582,7 +587,7 @@ MaskPtr DirectSumSpace::projection_onto_summands(std::optional> keep_indices, std::optional> discard_indices, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional device) const { auto kept = select_summand_indices(*this, std::move(keep_indices), std::move(discard_indices)); @@ -629,7 +634,7 @@ MaskPtr DirectSumSpace::inclusion_of_summands(std::optional> keep_indices, std::optional> discard_indices, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional device) const { auto proj = projection_onto_summands(std::move(keep_indices), @@ -647,7 +652,7 @@ DirectSumSpace::inclusion_of_summands(std::optional> kee MaskPtr DirectSumSpace::projection_onto_summand(SummandRef which, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional device) const { return projection_onto_summands(std::vector{ std::move(which) }, @@ -660,7 +665,7 @@ DirectSumSpace::projection_onto_summand(SummandRef which, MaskPtr DirectSumSpace::inclusion_of_summand(SummandRef which, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional device) const { return inclusion_of_summands(std::vector{ std::move(which) }, @@ -673,7 +678,7 @@ DirectSumSpace::inclusion_of_summand(SummandRef which, SymmetricTensorPtr DirectSumSpace::unit_vector_of_summand(int64 i, std::shared_ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) const { @@ -703,7 +708,7 @@ DirectSumSpace::unit_vector_of_summand(int64 i, "DirectSumSpace::unit_vector_of_summand labels must have length 1 " "(the fused DirectSumSpace leg)"); } - out->set_labels(*labels); + out->labels.set(*labels); } return out; } @@ -768,12 +773,12 @@ tensor_subgrid(TensorCPtr tensor, // Summand labels of the selected rows / cols, if the stacking leg is a DirectSumSpace. auto select_labels = [&](LegRef leg, - std::vector const& refs) -> std::optional { + std::vector const& refs) -> std::optional { auto dss = std::dynamic_pointer_cast(tensor->get_leg(leg)); if (!dss) { return std::nullopt; } - OptionalLabels out; + LegLabelList out; out.reserve(refs.size()); for (auto const& ref : refs) { out.push_back( @@ -792,7 +797,7 @@ tensor_subgrid(TensorCPtr tensor, } } return tensor_from_grid(std::move(grid), - tensor->labels(), + tensor->labels.as_list(), std::nullopt, select_labels(row_leg, rows), select_labels(col_leg, cols)); diff --git a/src/tensors/decompositions.cpp b/src/tensors/decompositions.cpp index 021ee2e5..ce98b506 100644 --- a/src/tensors/decompositions.cpp +++ b/src/tensors/decompositions.cpp @@ -134,26 +134,22 @@ make_python_charged_tensor(py::object invariant_part, py::object charged_state) } py::object -labels_to_py(OptionalLabels const& labels) +labels_to_py(LegLabelList const& labels) { py::list out; for (auto const& lab : labels) { - if (lab.has_value()) { - out.append(*lab); - } else { - out.append(py::none()); - } + out.append(lab); // empty string = unset } return out; } -OptionalLabels +LegLabelList leg_labels_from_py(py::object seq) { - OptionalLabels out; + LegLabelList out; for (auto item : py::reinterpret_borrow(seq)) { if (item.is_none()) { - out.push_back(std::nullopt); + out.push_back(""); } else { out.push_back(item.cast()); } @@ -162,12 +158,18 @@ leg_labels_from_py(py::object seq) } py::object -leg_label_to_py(OptionalLabel const& lab) +leg_label_to_py(std::string const& lab) +{ + return py::cast(lab); +} + +[[nodiscard]] std::string +py_label_as_string(py::handle obj) { - if (lab.has_value()) { - return py::cast(*lab); + if (obj.is_none()) { + return ""; } - return py::none(); + return obj.cast(); } template @@ -228,25 +230,25 @@ same_device2(py::object t1, py::object t2, std::string const& error_msg = "Incom return device; } -std::pair +std::pair parse_eigenvalue_labels(py::object new_labels) { py::object labels_iter = to_iterable(new_labels); py::ssize_t nlab = py::len(labels_iter); - OptionalLabel b; - OptionalLabel c; + std::string b; + std::string c; if (nlab == 0) { - b = std::nullopt; - c = std::nullopt; + b = ""; + c = ""; } else if (nlab == 1) { auto lab0 = labels_iter.attr("__getitem__")(0); - b = lab0.is_none() ? std::nullopt : std::optional(lab0.cast()); + b = py_label_as_string(lab0); c = _dual_leg_label(b); } else if (nlab == 2) { auto lab0 = labels_iter.attr("__getitem__")(0); auto lab1 = labels_iter.attr("__getitem__")(1); - b = lab0.is_none() ? std::nullopt : std::optional(lab0.cast()); - c = lab1.is_none() ? std::nullopt : std::optional(lab1.cast()); + b = py_label_as_string(lab0); + c = py_label_as_string(lab1); } else { throw std::invalid_argument(std::format( "Expected 0, 1 or 2 new_labels for eigenvalues. Got {}.", static_cast(nlab))); @@ -372,9 +374,9 @@ std::vector stripped_hidden_labels(py::object tensor) { std::vector res; - for (auto const& lab : leg_labels_from_py(tensor.attr("labels"))) { + for (auto const& lab : leg_labels_from_py(tensor.attr("labels").attr("as_list")())) { if (HiddenLegTensor::is_hidden_leg_label(lab)) { - res.push_back(*HiddenLegTensor::strip_hidden_prefix(lab)); + res.push_back(HiddenLegTensor::strip_hidden_prefix(lab)); } } return res; @@ -391,7 +393,7 @@ rehide_factor(py::object factor, std::vector const& hidden, int64 n if (hidden.empty()) { return factor; } - auto labs = leg_labels_from_py(factor.attr("labels")); + auto labs = leg_labels_from_py(factor.attr("labels").attr("as_list")()); auto num_legs = static_cast(labs.size()); if (new_leg_idx < 0) { new_leg_idx += num_legs; @@ -399,10 +401,10 @@ rehide_factor(py::object factor, std::vector const& hidden, int64 n std::vector> which; for (int64 i = 0; i < num_legs; ++i) { auto const& lab = labs[static_cast(i)]; - if (i == new_leg_idx || !lab.has_value()) { + if (i == new_leg_idx || lab.empty()) { continue; } - if (std::find(hidden.begin(), hidden.end(), *lab) != hidden.end()) { + if (std::find(hidden.begin(), hidden.end(), lab) != hidden.end()) { which.emplace_back(i); } } @@ -427,7 +429,7 @@ apply_mask_DiagonalTensor_py(py::object tensor, py::object mask) auto init = parse_tensor_init(py::make_tuple(mask.attr("small_leg")), py::make_tuple(mask.attr("small_leg")), backend, - tensor.attr("labels"), + tensor.attr("labels").attr("as_list")(), true); auto dt = SymmetricTensor::_parse_default_dtype(std::nullopt, init.symmetry); if (!dt.has_value()) { @@ -444,7 +446,7 @@ apply_mask_DiagonalTensor_py(py::object tensor, py::object mask) auto data = backend->apply_mask_to_DiagonalTensor(tensor.cast(), mask.cast()); return make_python_diagonal_tensor( - std::move(data), mask.attr("small_leg"), backend, tensor.attr("labels")); + std::move(data), mask.attr("small_leg"), backend, tensor.attr("labels").attr("as_list")()); } std::tuple @@ -459,28 +461,22 @@ eigh_py(py::object tensor, py::object new_labels, bool new_leg_dual, py::object // --- py::object labels_iter = to_iterable(new_labels); py::ssize_t nlab = py::len(labels_iter); - OptionalLabel a; - OptionalLabel b; - OptionalLabel c; + std::string a; + std::string b; + std::string c; if (nlab == 1) { - a = c = labels_iter.attr("__getitem__")(0).is_none() - ? std::nullopt - : std::optional(labels_iter.attr("__getitem__")(0).cast()); + a = c = py_label_as_string(labels_iter.attr("__getitem__")(0)); b = _dual_leg_label(a); } else if (nlab == 2) { - a = c = labels_iter.attr("__getitem__")(0).is_none() - ? std::nullopt - : std::optional(labels_iter.attr("__getitem__")(0).cast()); - b = labels_iter.attr("__getitem__")(1).is_none() - ? std::nullopt - : std::optional(labels_iter.attr("__getitem__")(1).cast()); + a = c = py_label_as_string(labels_iter.attr("__getitem__")(0)); + b = py_label_as_string(labels_iter.attr("__getitem__")(1)); } else if (nlab == 3) { auto lab0 = labels_iter.attr("__getitem__")(0); auto lab1 = labels_iter.attr("__getitem__")(1); auto lab2 = labels_iter.attr("__getitem__")(2); - a = lab0.is_none() ? std::nullopt : std::optional(lab0.cast()); - b = lab1.is_none() ? std::nullopt : std::optional(lab1.cast()); - c = lab2.is_none() ? std::nullopt : std::optional(lab2.cast()); + a = py_label_as_string(lab0); + b = py_label_as_string(lab1); + c = py_label_as_string(lab2); } else { throw std::invalid_argument( std::format("Expected 1, 2 or 3 new_labels. Got {}.", static_cast(nlab))); @@ -494,21 +490,18 @@ eigh_py(py::object tensor, py::object new_labels, bool new_leg_dual, py::object throw NotImplemented("eigh for ChargedTensor"); } if (is_DiagonalTensor(tensor)) { - OptionalLabel cod_lab = - tensor.attr("codomain_labels").attr("__getitem__")(0).is_none() - ? std::nullopt - : std::optional( - tensor.attr("codomain_labels").attr("__getitem__")(0).cast()); + std::string cod_lab = + tensor.attr("codomain_labels").attr("__getitem__")(0).cast(); auto leg_sp = as_space_leg(tensor.attr("leg")); auto V = py::cast(SymmetricTensor::from_eye(std::make_shared(std::vector{ std::dynamic_pointer_cast(leg_sp) }), tensor.attr("backend").cast(), - OptionalLabels{ cod_lab, a }, + LegLabelList{ cod_lab, a }, tensor.attr("dtype").cast(), tensor.attr("device").cast())); - py::object W = tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true) - .attr("set_labels")(py_list(leg_label_to_py(b), leg_label_to_py(c))); + py::object W = tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true); + W.attr("labels").attr("set")(py_list(leg_label_to_py(b), leg_label_to_py(c))); return { W, V }; } tensor = tensor.attr("as_SymmetricTensor")(); @@ -569,28 +562,22 @@ eig_py(py::object tensor, py::object new_labels, bool new_leg_dual, py::object s { py::object labels_iter = to_iterable(new_labels); py::ssize_t nlab = py::len(labels_iter); - OptionalLabel a; - OptionalLabel b; - OptionalLabel c; + std::string a; + std::string b; + std::string c; if (nlab == 1) { - a = c = labels_iter.attr("__getitem__")(0).is_none() - ? std::nullopt - : std::optional(labels_iter.attr("__getitem__")(0).cast()); + a = c = py_label_as_string(labels_iter.attr("__getitem__")(0)); b = _dual_leg_label(a); } else if (nlab == 2) { - a = c = labels_iter.attr("__getitem__")(0).is_none() - ? std::nullopt - : std::optional(labels_iter.attr("__getitem__")(0).cast()); - b = labels_iter.attr("__getitem__")(1).is_none() - ? std::nullopt - : std::optional(labels_iter.attr("__getitem__")(1).cast()); + a = c = py_label_as_string(labels_iter.attr("__getitem__")(0)); + b = py_label_as_string(labels_iter.attr("__getitem__")(1)); } else if (nlab == 3) { auto lab0 = labels_iter.attr("__getitem__")(0); auto lab1 = labels_iter.attr("__getitem__")(1); auto lab2 = labels_iter.attr("__getitem__")(2); - a = lab0.is_none() ? std::nullopt : std::optional(lab0.cast()); - b = lab1.is_none() ? std::nullopt : std::optional(lab1.cast()); - c = lab2.is_none() ? std::nullopt : std::optional(lab2.cast()); + a = py_label_as_string(lab0); + b = py_label_as_string(lab1); + c = py_label_as_string(lab2); } else { throw std::invalid_argument( std::format("Expected 1, 2 or 3 new_labels. Got {}.", static_cast(nlab))); @@ -603,21 +590,18 @@ eig_py(py::object tensor, py::object new_labels, bool new_leg_dual, py::object s throw std::invalid_argument("eig requires matching domain and codomain"); } if (is_DiagonalTensor(tensor)) { - OptionalLabel cod_lab = - tensor.attr("codomain_labels").attr("__getitem__")(0).is_none() - ? std::nullopt - : std::optional( - tensor.attr("codomain_labels").attr("__getitem__")(0).cast()); + std::string cod_lab = + tensor.attr("codomain_labels").attr("__getitem__")(0).cast(); auto leg_sp = as_space_leg(tensor.attr("leg")); auto V = py::cast(SymmetricTensor::from_eye(std::make_shared(std::vector{ std::dynamic_pointer_cast(leg_sp) }), tensor.attr("backend").cast(), - OptionalLabels{ cod_lab, a }, + LegLabelList{ cod_lab, a }, tensor.attr("dtype").cast(), tensor.attr("device").cast())); - py::object W = tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true) - .attr("set_labels")(py_list(leg_label_to_py(b), leg_label_to_py(c))); + py::object W = tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true); + W.attr("labels").attr("set")(py_list(leg_label_to_py(b), leg_label_to_py(c))); return { W, V }; } @@ -654,8 +638,9 @@ eigvalsh_py(py::object tensor, py::object new_labels, bool new_leg_dual, py::obj throw std::invalid_argument("eigvalsh requires matching domain and codomain"); } if (is_DiagonalTensor(tensor)) { - return tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true) - .attr("set_labels")(py_list(leg_label_to_py(b), leg_label_to_py(c))); + py::object W = tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true); + W.attr("labels").attr("set")(py_list(leg_label_to_py(b), leg_label_to_py(c))); + return W; } auto prep = prepare_square_eigen(tensor, new_leg_dual, "eigvalsh"); auto [w_data, new_leg] = prep.backend->eigvalsh( @@ -680,8 +665,9 @@ eigvals_py(py::object tensor, py::object new_labels, bool new_leg_dual, py::obje throw std::invalid_argument("eigvals requires matching domain and codomain"); } if (is_DiagonalTensor(tensor)) { - return tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true) - .attr("set_labels")(py_list(leg_label_to_py(b), leg_label_to_py(c))); + py::object W = tensor.attr("as_DiagonalTensor")(py::arg("guarantee_copy") = true); + W.attr("labels").attr("set")(py_list(leg_label_to_py(b), leg_label_to_py(c))); + return W; } auto prep = prepare_square_eigen(tensor, new_leg_dual, "eigvals"); auto [w_data, new_leg] = prep.backend->eigvals( @@ -873,7 +859,7 @@ svd_py(py::object tensor, return { U, S, Vh }; } - std::optional svd_labs; + std::optional svd_labs; if (!new_labels.is_none()) { svd_labs = leg_labels_from_py(to_iterable(new_labels)); } @@ -956,14 +942,9 @@ truncate_singular_values_py(py::object S, std::optional{ svd_min }, minimize_error); if (mask_labels.is_none()) { - py::object lab0 = S.attr("labels").attr("__getitem__")(0); - OptionalLabel dual; - if (lab0.is_none()) { - dual = _dual_leg_label(std::nullopt); - } else { - dual = _dual_leg_label(lab0.cast()); - } - mask_labels = py_list(lab0, leg_label_to_py(dual)); + std::string lab0 = py_label_as_string(S.attr("labels").attr("__getitem__")(0)); + std::string dual = _dual_leg_label(lab0); + mask_labels = py_list(leg_label_to_py(lab0), leg_label_to_py(dual)); } py::object mask = make_python_mask(std::move(mask_data), S.attr("leg"), @@ -1016,26 +997,22 @@ truncated_svd_py(py::object tensor, namespace { py::object -labels_to_py_opt(std::optional const& labels) +labels_to_py_opt(std::optional const& labels) { if (!labels.has_value()) { return py::none(); } py::list out; for (auto const& lab : *labels) { - if (lab.has_value()) { - out.append(*lab); - } else { - out.append(py::none()); - } + out.append(lab); } return out; } py::object -labels_to_py_opt(OptionalLabels const& labels) +labels_to_py_opt(LegLabelList const& labels) { - return labels_to_py_opt(std::optional{ labels }); + return labels_to_py_opt(std::optional{ labels }); } py::object @@ -1068,7 +1045,7 @@ apply_mask_DiagonalTensor(DiagonalTensorCPtr tensor, MaskCPtr mask) std::tuple eigh(TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual, std::optional sort) { @@ -1078,10 +1055,7 @@ eigh(TensorCPtr tensor, } std::tuple -eig(TensorCPtr tensor, - OptionalLabels new_labels, - bool new_leg_dual, - std::optional sort) +eig(TensorCPtr tensor, LegLabelList new_labels, bool new_leg_dual, std::optional sort) { py::object sort_py = sort.has_value() ? py::cast(*sort) : py::none(); auto [W, V] = eig_py(py::cast(tensor), labels_to_py_opt(new_labels), new_leg_dual, sort_py); @@ -1090,7 +1064,7 @@ eig(TensorCPtr tensor, DiagonalTensorPtr eigvalsh(TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual, std::optional sort) { @@ -1101,7 +1075,7 @@ eigvalsh(TensorCPtr tensor, DiagonalTensorPtr eigvals(TensorCPtr tensor, - OptionalLabels new_labels, + LegLabelList new_labels, bool new_leg_dual, std::optional sort) { @@ -1141,7 +1115,7 @@ entropy(DiagonalTensorCPtr p, float64 n) std::tuple lq(TensorCPtr tensor, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, bool charge_leg_top) { @@ -1152,7 +1126,7 @@ lq(TensorCPtr tensor, std::tuple qr(TensorCPtr tensor, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, bool charge_leg_top) { @@ -1163,7 +1137,7 @@ qr(TensorCPtr tensor, std::tuple svd(TensorCPtr tensor, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, bool charge_leg_top, std::optional algorithm) @@ -1191,7 +1165,7 @@ truncate_singular_values(DiagonalTensorCPtr S, float64 trunc_cut, float64 svd_min, bool minimize_error, - std::optional mask_labels) + std::optional mask_labels) { auto [mask, err, new_norm] = truncate_singular_values_py(py::cast(S), chi_max, @@ -1206,7 +1180,7 @@ truncate_singular_values(DiagonalTensorCPtr S, std::tuple truncated_svd(TensorCPtr tensor, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, bool charge_leg_top, std::optional algorithm, diff --git a/src/tensors/diagonal_tensor.cpp b/src/tensors/diagonal_tensor.cpp index 53f93ca3..1928b1b4 100644 --- a/src/tensors/diagonal_tensor.cpp +++ b/src/tensors/diagonal_tensor.cpp @@ -57,7 +57,7 @@ DiagonalTensor::DiagonalTensor(TensorBackend::DataPtr data_in, Space::Ptr leg_in, TensorBackend::Ptr backend_in, Symmetry::Ptr symmetry_in, - OptionalLabels labels_in) + LegLabelList labels_in) : SymmetricTensor(std::move(data_in), std::make_shared( std::vector{ std::dynamic_pointer_cast(leg_in) }), @@ -118,7 +118,7 @@ DiagonalTensor::Ptr DiagonalTensor::from_block_func(BlockFactoryFn func, Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -153,7 +153,7 @@ DiagonalTensor::Ptr DiagonalTensor::from_dense_block(BlockBackend::BlockPtr block, Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, float64 tol, std::optional device, @@ -184,7 +184,7 @@ DiagonalTensor::Ptr DiagonalTensor::from_diag_block(BlockBackend::BlockPtr diag, Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device, float64 tol) @@ -209,7 +209,7 @@ DiagonalTensor::from_diag_block(BlockBackend::BlockPtr diag, DiagonalTensor::Ptr DiagonalTensor::from_eye(Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -233,7 +233,7 @@ DiagonalTensor::from_random_normal(Space::Ptr leg, TensorCPtr mean, float64 sigma, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -261,7 +261,7 @@ DiagonalTensor::from_random_normal(Space::Ptr leg, } } if (!labels.has_value()) { - labels = mean->labels(); + labels = mean->labels.as_list(); } if (!device.has_value()) { device = mean->device; @@ -296,7 +296,7 @@ DiagonalTensor::from_random_normal(Space::Ptr leg, with_zero_mean->leg(), backend, with_zero_mean->symmetry, - with_zero_mean->labels()); + with_zero_mean->labels.as_list()); } return with_zero_mean; } @@ -304,7 +304,7 @@ DiagonalTensor::from_random_normal(Space::Ptr leg, DiagonalTensor::Ptr DiagonalTensor::from_random_uniform(Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -327,7 +327,7 @@ DiagonalTensor::Ptr DiagonalTensor::from_sector_block_func(SectorBlockFactoryFn func, Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -363,13 +363,13 @@ DiagonalTensor::from_tensor(SymmetricTensorCPtr tens, std::optional tol auto data = tens->backend->diagonal_tensor_from_full_tensor(tens, tol); auto leg = as_space(tens->codomain->factors[0]); return std::make_shared( - data, leg, tens->backend, tens->symmetry, tens->labels()); + data, leg, tens->backend, tens->symmetry, tens->labels.as_list()); } DiagonalTensor::Ptr DiagonalTensor::from_zero(Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -396,7 +396,7 @@ DiagonalTensor::as_dtype(Dtype new_dtype) return shared_from_this(); } auto new_data = backend->to_dtype(shared_from_this(), new_dtype); - return std::make_shared(new_data, leg(), backend, symmetry, labels()); + return std::make_shared(new_data, leg(), backend, symmetry, labels.as_list()); } SymmetricTensorPtr @@ -408,7 +408,7 @@ DiagonalTensor::as_SymmetricTensor(bool /*guarantee_copy*/, std::optionalfull_data_from_diagonal_tensor( std::static_pointer_cast(shared_from_this())); return std::make_shared( - new_data, codomain, domain, backend, symmetry, labels()); + new_data, codomain, domain, backend, symmetry, labels.as_list()); } DiagonalTensor::Ptr @@ -443,7 +443,7 @@ DiagonalTensor::_binary_operand(BlockBackend::Scalar other, }, /*maps_zero_to_zero=*/false); } - return std::make_shared(new_data, leg(), backend, symmetry, labels()); + return std::make_shared(new_data, leg(), backend, symmetry, labels.as_list()); } DiagonalTensor::Ptr @@ -474,7 +474,7 @@ DiagonalTensor::_binary_operand(DiagonalTensorCPtr other, func, /*partial_zero_is_zero=*/false); } - auto out_labels = _get_matching_labels(labels(), other->labels()); + auto out_labels = _get_matching_labels(labels.as_list(), other->labels.as_list()); return std::make_shared(new_data, leg(), same, symmetry, out_labels); } @@ -495,7 +495,7 @@ DiagonalTensor::copy(bool deep, } else { new_data = data; } - return std::make_shared(new_data, leg(), backend, symmetry, labels()); + return std::make_shared(new_data, leg(), backend, symmetry, labels.as_list()); } DiagonalTensorPtr @@ -543,7 +543,7 @@ DiagonalTensor::elementwise_almost_equal(DiagonalTensorCPtr other, float64 rtol, if (auto id = std::dynamic_pointer_cast(other)) { other = std::const_pointer_cast(id)->as_DiagonalTensor(); } - auto ones = from_eye(leg(), backend, labels(), dtype::to_real(dtype), device); + auto ones = from_eye(leg(), backend, labels.as_list(), dtype::to_real(dtype), device); auto self_m = std::static_pointer_cast(shared_from_this()); BlockBinaryFn sub = [](BlockBackend::BlockPtr const& a, BlockBackend::BlockPtr const& b) { return (*a) - (*b); @@ -584,7 +584,7 @@ DiagonalTensor::_elementwise_binary(DiagonalTensorCPtr other, other, std::move(func), partial_zero_is_zero); - auto labs = _get_matching_labels(labels(), other->labels()); + auto labs = _get_matching_labels(labels.as_list(), other->labels.as_list()); return std::make_shared(data_out, leg(), same, symmetry, labs); } @@ -595,7 +595,7 @@ DiagonalTensor::_elementwise_unary(BlockUnaryFn func, bool maps_zero_to_zero) std::static_pointer_cast(shared_from_this()), std::move(func), maps_zero_to_zero); - return std::make_shared(data_out, leg(), backend, symmetry, labels()); + return std::make_shared(data_out, leg(), backend, symmetry, labels.as_list()); } BlockBackend::Scalar @@ -751,7 +751,7 @@ DiagonalTensor::sector_argmin(std::optional s) const auto selected_sector = sectors[*winning_sector]; auto selected_local_argmin = winning_local_argmin; auto device_s = device; - auto mask_labels = labels(); + auto mask_labels = labels.as_list(); mask_labels.resize(1); mask_labels.push_back(_dual_leg_label(mask_labels[0])); auto diagonal = from_sector_block_func( @@ -883,7 +883,8 @@ DiagonalTensor::to_backend(TensorBackend::Ptr new_backend, new_backend->block_backend->as_block(py::cast(old_diag), dtype_opt, device_s); new_data = new_backend->diagonal_from_block(new_diag, codomain, 0.); } - return std::make_shared(new_data, leg(), new_backend, symmetry, labels()); + return std::make_shared( + new_data, leg(), new_backend, symmetry, labels.as_list()); } BlockBackend::BlockPtr @@ -906,7 +907,7 @@ DiagonalTensor::to_dense_block( auto diag = diagonal_as_block(dtype_opt); auto res = backend->block_backend->block_from_diagonal(diag); if (leg_order.has_value()) { - res = backend->block_backend->permute_axes(res, get_leg_idcs(*leg_order)); + res = backend->block_backend->permute_axes(res, labels.indices(*leg_order)); } return res; } @@ -936,10 +937,10 @@ DiagonalTensor::from_hdf5(lasyten::hdf5::Loader& loader, H5Idec_ref(id); } auto dt = hdf5_export::load_dtype_string(loader, subpath + "dtype"); - auto labels = hdf5_export::load_optional_labels(loader, subpath + "labels"); + auto labels = hdf5_export::load_leg_labels(loader, subpath + "labels"); int64 nlegs = codomain->num_factors + domain->num_factors; if (labels.empty() && nlegs > 0) { - labels.assign(static_cast(nlegs), std::nullopt); + labels.assign(static_cast(nlegs), ""); } auto obj = std::make_shared( @@ -972,7 +973,7 @@ Identity::unsupported_factory(char const* name) Identity::Identity(Space::Ptr leg_in, TensorBackend::Ptr backend_in, Symmetry::Ptr symmetry_in, - OptionalLabels labels_in, + LegLabelList labels_in, Dtype dtype_in, std::string device_in) : DiagonalTensor( @@ -1005,7 +1006,7 @@ Identity::class_name() const Identity::Ptr Identity::from_eye(Space::Ptr leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -1040,9 +1041,9 @@ Identity::from_hdf5(lasyten::hdf5::Loader& loader, auto device = loader.load_string(device_id); H5Idec_ref(device_id); auto dt = hdf5_export::load_dtype_string(loader, subpath + "dtype"); - auto labels = hdf5_export::load_optional_labels(loader, subpath + "labels"); + auto labels = hdf5_export::load_leg_labels(loader, subpath + "labels"); if (labels.empty()) { - labels.assign(2, std::nullopt); + labels.assign(2, ""); } if (dynamic_cast(backend->block_backend.get()) != nullptr) { @@ -1064,7 +1065,8 @@ Identity::as_dtype(Dtype new_dtype) if (new_dtype == dtype) { return shared_from_this(); } - return std::make_shared(leg(), backend, symmetry, labels(), new_dtype, device); + return std::make_shared( + leg(), backend, symmetry, labels.as_list(), new_dtype, device); } SymmetricTensorPtr @@ -1073,7 +1075,7 @@ Identity::as_SymmetricTensor(bool /*guarantee_copy*/, std::optional if (warning.has_value()) { warn(*warning); } - return SymmetricTensor::from_eye(codomain, backend, labels(), dtype, device); + return SymmetricTensor::from_eye(codomain, backend, labels.as_list(), dtype, device); } DiagonalTensor::Ptr @@ -1082,7 +1084,7 @@ Identity::as_DiagonalTensor(bool /*guarantee_copy*/, std::optional if (warning.has_value()) { warn(*warning); } - return DiagonalTensor::from_eye(leg(), backend, labels(), dtype, device); + return DiagonalTensor::from_eye(leg(), backend, labels.as_list(), dtype, device); } DiagonalTensor::Ptr @@ -1244,7 +1246,8 @@ Identity::to_backend(TensorBackend::Ptr new_backend, Dtype dt = dtype_opt.value_or(dtype); auto device_s = new_backend->block_backend->as_device( device_opt.has_value() ? device_opt : std::optional{ device }); - return std::make_shared(leg(), new_backend, symmetry, labels(), dt, device_s); + return std::make_shared( + leg(), new_backend, symmetry, labels.as_list(), dt, device_s); } BlockBackend::BlockPtr diff --git a/src/tensors/helpers.cpp b/src/tensors/helpers.cpp index a32ef1d4..2c7bfcac 100644 --- a/src/tensors/helpers.cpp +++ b/src/tensors/helpers.cpp @@ -20,18 +20,18 @@ namespace lasyten { namespace { -OptionalLabels -apply_relabel(OptionalLabels labels, +LegLabelList +apply_relabel(LegLabelList labels, std::optional> const& relabel) { if (!relabel.has_value()) { return labels; } for (auto& lab : labels) { - if (!lab.has_value()) { + if (lab.empty()) { continue; } - auto it = relabel->find(*lab); + auto it = relabel->find(lab); if (it != relabel->end()) { lab = it->second; } @@ -40,16 +40,16 @@ apply_relabel(OptionalLabels labels, } std::unordered_set -duplicate_label_entries(OptionalLabels const& labels) +duplicate_label_entries(LegLabelList const& labels) { std::unordered_set seen; std::unordered_set dups; for (auto const& lab : labels) { - if (!lab.has_value()) { + if (lab.empty()) { continue; } - if (!seen.insert(*lab).second) { - dups.insert(*lab); + if (!seen.insert(lab).second) { + dups.insert(lab); } } return dups; @@ -254,12 +254,13 @@ _compose_with_Mask(TensorCPtr tensor, MaskCPtr mask, int64 leg_idx) } auto& [data, codomain, domain] = contracted; // use the labels of `tensor`, which keep the ``!`` of hidden legs -> HiddenLegTensor result - return HiddenLegTensor::maybe_wrap(std::make_shared(std::move(data), - std::move(codomain), - std::move(domain), - backend, - tens->symmetry, - tensor->labels())); + return HiddenLegTensor::maybe_wrap( + std::make_shared(std::move(data), + std::move(codomain), + std::move(domain), + backend, + tens->symmetry, + tensor->labels.as_list())); } std::variant @@ -277,10 +278,10 @@ _compose_SymmetricTensors(SymmetricTensorCPtr tensor1, return backend->inner(tensor1, tensor2, /*do_dagger=*/false); } - OptionalLabels labels_codomain = apply_relabel(tensor1->codomain_labels(), relabel1); - OptionalLabels labels_domain = apply_relabel(tensor2->domain_labels(), relabel2); + LegLabelList labels_codomain = apply_relabel(tensor1->codomain_labels(), relabel1); + LegLabelList labels_domain = apply_relabel(tensor2->domain_labels(), relabel2); - OptionalLabels labels = labels_codomain; + LegLabelList labels = labels_codomain; for (auto it = labels_domain.rbegin(); it != labels_domain.rend(); ++it) { labels.push_back(*it); } @@ -288,7 +289,7 @@ _compose_SymmetricTensors(SymmetricTensorCPtr tensor1, auto duplicates = duplicate_label_entries(labels); int64 dup_counter = 0; for (auto& lab : labels) { - if (lab.has_value() && duplicates.contains(*lab)) { + if (!lab.empty() && duplicates.contains(lab)) { lab = std::format("?{}", dup_counter); ++dup_counter; } @@ -650,11 +651,11 @@ _decomposition_prepare(TensorCPtr tensor, bool new_leg_dual) return { tens, new_co_domain, combine_codomain, combine_domain }; } -std::pair -_decomposition_labels(OptionalLabels const& new_labels) +std::pair +_decomposition_labels(LegLabelList const& new_labels) { if (new_labels.size() == 1) { - OptionalLabel a = new_labels[0]; + std::string a = new_labels[0]; return { a, _dual_leg_label(a) }; } if (new_labels.size() == 2) { @@ -663,14 +664,14 @@ _decomposition_labels(OptionalLabels const& new_labels) throw std::invalid_argument(std::format("Expected 1 or 2 labels. Got {}", new_labels.size())); } -std::tuple -_svd_new_labels(std::optional new_labels) +std::tuple +_svd_new_labels(std::optional new_labels) { if (!new_labels.has_value()) { - return { std::nullopt, std::nullopt, std::nullopt, std::nullopt }; + return { "", "", "", "" }; } - OptionalLabels const& labels = *new_labels; - OptionalLabel a, b, c, d; + LegLabelList const& labels = *new_labels; + std::string a, b, c, d; if (labels.size() == 1) { a = c = labels[0]; b = d = _dual_leg_label(labels[0]); @@ -686,7 +687,7 @@ _svd_new_labels(std::optional new_labels) throw std::invalid_argument( std::format("Expected 1, 2 or 4 new_labels. Got {}", labels.size())); } - if (b.has_value() && c.has_value() && *b == *c) { + if (!b.empty() && !c.empty() && b == c) { throw std::invalid_argument( "new_labels for SVD must not reuse the same label on both sides"); } diff --git a/src/tensors/hidden_leg_tensor.cpp b/src/tensors/hidden_leg_tensor.cpp index af538ee6..142357a9 100644 --- a/src/tensors/hidden_leg_tensor.cpp +++ b/src/tensors/hidden_leg_tensor.cpp @@ -20,17 +20,17 @@ namespace { [[nodiscard]] bool is_dual_pair(std::string const& a, std::string const& b) { - auto dual_a = _dual_leg_label(OptionalLabel{ a }); - return dual_a && *dual_a == b; + return _dual_leg_label(a) == b; } void -check_public_labels_no_bang(OptionalLabels const& labs) +check_public_labels_no_bang(LegLabelList const& labs) { for (auto const& lab : labs) { - if (lab && !HiddenLegTensor::is_hidden_leg_label(lab) && label_contains_exclamation(lab)) { + if (!lab.empty() && !HiddenLegTensor::is_hidden_leg_label(lab) && + label_contains_exclamation(lab)) { throw std::invalid_argument(std::format( - "Public label '{}' must not contain '{}'", *lab, HiddenLegTensor::HIDDEN_PREFIX)); + "Public label '{}' must not contain '{}'", lab, HiddenLegTensor::HIDDEN_PREFIX)); } } } @@ -38,13 +38,13 @@ check_public_labels_no_bang(OptionalLabels const& labs) } // namespace bool -label_contains_exclamation(OptionalLabel const& label) +label_contains_exclamation(std::string const& label) { - return label && label->find(HiddenLegTensor::HIDDEN_PREFIX) != std::string::npos; + return label.find(HiddenLegTensor::HIDDEN_PREFIX) != std::string::npos; } void -reject_exclamation_in_labels(OptionalLabels const& labels, std::string const& context) +reject_exclamation_in_labels(LegLabelList const& labels, std::string const& context) { for (auto const& lab : labels) { if (label_contains_exclamation(lab)) { @@ -53,23 +53,23 @@ reject_exclamation_in_labels(OptionalLabels const& labels, std::string const& co "Use HiddenLegTensor to hide legs, or ChargedTensor for a charge leg.", context, HiddenLegTensor::HIDDEN_PREFIX, - *lab)); + lab)); } } } bool -HiddenLegTensor::is_charge_temp_label(OptionalLabel const& label) +HiddenLegTensor::is_charge_temp_label(std::string const& label) { // ChargedTensor invariant-part markers and short-lived labels during compose/inner: // "!", "!1"/"!2", "!A"/"!B", and their duals "!A*"/"!B*". - if (!label || label->empty() || (*label)[0] != HIDDEN_PREFIX) { + if (label.empty() || label[0] != HIDDEN_PREFIX) { return false; } - if (label->size() == 1) { + if (label.size() == 1) { return true; } - std::string_view suffix(label->begin() + 1, label->end()); + std::string_view suffix(label.begin() + 1, label.end()); if (std::all_of( suffix.begin(), suffix.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) { return true; @@ -85,28 +85,28 @@ HiddenLegTensor::is_charge_temp_label(OptionalLabel const& label) } bool -HiddenLegTensor::is_hidden_leg_label(OptionalLabel const& label) +HiddenLegTensor::is_hidden_leg_label(std::string const& label) { // Hidden labels are '!' plus a nonempty name (e.g. '!p', '!charge*', '!(p.q)'). - if (!label || label->size() <= 1 || (*label)[0] != HIDDEN_PREFIX) { + if (label.size() <= 1 || label[0] != HIDDEN_PREFIX) { return false; } return !is_charge_temp_label(label); } bool -HiddenLegTensor::has_hidden_leg_labels(OptionalLabels const& labels) +HiddenLegTensor::has_hidden_leg_labels(LegLabelList const& labels) { return std::ranges::any_of(labels, [](auto const& l) { return is_hidden_leg_label(l); }); } -OptionalLabel -HiddenLegTensor::strip_hidden_prefix(OptionalLabel const& label) +std::string +HiddenLegTensor::strip_hidden_prefix(std::string const& label) { if (!is_hidden_leg_label(label)) { return label; } - return label->substr(1); + return label.substr(1); } std::string @@ -127,12 +127,12 @@ HiddenLegTensor::add_hidden_prefix(std::string const& label) } void -HiddenLegTensor::validate_no_dual_hidden_pair(OptionalLabels const& labels) +HiddenLegTensor::validate_no_dual_hidden_pair(LegLabelList const& labels) { std::vector hidden; for (auto const& lab : labels) { if (is_hidden_leg_label(lab)) { - hidden.push_back(*lab); + hidden.push_back(lab); } } for (std::size_t i = 0; i < hidden.size(); ++i) { @@ -158,15 +158,26 @@ HiddenLegTensor::HiddenLegTensor(SymmetricTensor::Ptr tensor) tensor->domain, tensor->backend, tensor->symmetry, - tensor->labels(), + tensor->labels.as_list(), /*check_complex_dtype=*/false) { - if (!has_hidden_leg_labels(labels())) { + if (!has_hidden_leg_labels(labels.as_list())) { throw std::invalid_argument( "HiddenLegTensor adopting constructor requires at least one '!'-prefixed label"); } - validate_no_dual_hidden_pair(labels()); - check_public_labels_no_bang(labels()); + validate_no_dual_hidden_pair(labels.as_list()); + check_public_labels_no_bang(labels.as_list()); + + labels.set_hooks( + [](LegLabels& /*self*/, LegLabelList const& proposed) { + if (!has_hidden_leg_labels(proposed)) { + throw std::invalid_argument( + "HiddenLegTensor must keep at least one '!'-prefixed hidden leg label"); + } + validate_no_dual_hidden_pair(proposed); + check_public_labels_no_bang(proposed); + }, + [](LegLabels& /*self*/) {}); } HiddenLegTensor::Ptr @@ -180,18 +191,18 @@ HiddenLegTensor::from_tensor(Tensor::Ptr tensor, if (auto existing = std::dynamic_pointer_cast(sym)) { sym = existing->unhide_legs(); } - auto idcs = sym->get_leg_idcs(which_legs); + auto idcs = sym->labels.indices(which_legs); std::unordered_set hide_set(idcs.begin(), idcs.end()); if (hide_set.size() != idcs.size()) { throw std::invalid_argument("Duplicate legs in which_legs for HiddenLegTensor"); } - auto labs = sym->labels(); + auto labs = sym->labels.as_list(); for (auto idx : idcs) { auto& lab = labs[static_cast(idx)]; - if (!lab) { + if (lab.empty()) { throw std::invalid_argument(std::format("Cannot hide unlabeled leg at index {}", idx)); } - *lab = add_hidden_prefix(*lab); + lab = add_hidden_prefix(lab); } validate_no_dual_hidden_pair(labs); check_public_labels_no_bang(labs); @@ -204,9 +215,9 @@ void HiddenLegTensor::test_sanity() const { SymmetricTensor::test_sanity(); - assert(has_hidden_leg_labels(labels())); - validate_no_dual_hidden_pair(labels()); - check_public_labels_no_bang(labels()); + assert(has_hidden_leg_labels(labels.as_list())); + validate_no_dual_hidden_pair(labels.as_list()); + check_public_labels_no_bang(labels.as_list()); } std::string @@ -225,7 +236,7 @@ std::vector HiddenLegTensor::hidden_leg_idcs() const { std::vector out; - auto labs = labels(); + auto labs = labels.as_list(); for (int64 i = 0; i < static_cast(labs.size()); ++i) { if (is_hidden_leg_label(labs[static_cast(i)])) { out.push_back(i); @@ -238,7 +249,7 @@ std::vector HiddenLegTensor::public_leg_idcs() const { std::vector out; - auto labs = labels(); + auto labs = labels.as_list(); for (int64 i = 0; i < static_cast(labs.size()); ++i) { if (!is_hidden_leg_label(labs[static_cast(i)])) { out.push_back(i); @@ -250,7 +261,7 @@ HiddenLegTensor::public_leg_idcs() const SymmetricTensorPtr HiddenLegTensor::unhide_legs() const { - auto labs = labels(); + auto labs = labels.as_list(); for (auto& lab : labs) { lab = strip_hidden_prefix(lab); } @@ -299,8 +310,8 @@ HiddenLegTensor::dagger() const { auto self = std::dynamic_pointer_cast(shared_from_this()); auto new_data = backend->dagger(self); - OptionalLabels dual_labs; - auto labs = labels(); + LegLabelList dual_labs; + auto labs = labels.as_list(); dual_labs.reserve(labs.size()); for (auto it = labs.rbegin(); it != labs.rend(); ++it) { dual_labs.push_back(_dual_leg_label(*it)); @@ -326,36 +337,6 @@ HiddenLegTensor::to_backend(TensorBackend::Ptr new_backend, return std::make_shared(std::move(base)); } -LabelledLegs& -HiddenLegTensor::set_label(int64 pos, OptionalLabel label) -{ - pos = to_valid_idx(pos, num_legs); - if (label && !is_hidden_leg_label(label) && label_contains_exclamation(label)) { - throw std::invalid_argument( - std::format("Public label '{}' must not contain '{}'", *label, HIDDEN_PREFIX)); - } - LabelledLegs::set_label(pos, label); - validate_no_dual_hidden_pair(labels()); - if (!has_hidden_leg_labels(labels())) { - throw std::invalid_argument( - "HiddenLegTensor must keep at least one '!'-prefixed hidden leg label"); - } - return *this; -} - -Tensor& -HiddenLegTensor::set_labels(OptionalLabels labels_in) -{ - if (!has_hidden_leg_labels(labels_in)) { - throw std::invalid_argument( - "HiddenLegTensor.set_labels requires at least one '!'-prefixed label"); - } - validate_no_dual_hidden_pair(labels_in); - check_public_labels_no_bang(labels_in); - Tensor::set_labels(std::move(labels_in)); - return *this; -} - TensorPtr HiddenLegTensor::maybe_wrap(SymmetricTensor::Ptr tensor) { @@ -365,7 +346,7 @@ HiddenLegTensor::maybe_wrap(SymmetricTensor::Ptr tensor) if (std::dynamic_pointer_cast(tensor)) { return tensor; } - if (has_hidden_leg_labels(tensor->labels())) { + if (has_hidden_leg_labels(tensor->labels.as_list())) { return std::make_shared(std::move(tensor)); } return tensor; diff --git a/src/tensors/labels.cpp b/src/tensors/labels.cpp index db88cc17..4fb3a27b 100644 --- a/src/tensors/labels.cpp +++ b/src/tensors/labels.cpp @@ -1,8 +1,10 @@ -#include #include +#include + #include #include +#include #include #include #include @@ -13,31 +15,25 @@ namespace lasyten { namespace { -/// Duplicate entries in ``seq``, ignoring unset (``nullopt``) labels. +/// Duplicate entries in ``seq``, ignoring unset (``""``) labels. [[nodiscard]] std::unordered_set -duplicate_leg_labels(OptionalLabels const& seq) +duplicate_leg_labels(LegLabelList const& seq) { std::unordered_set seen; std::unordered_set dups; for (auto const& ele : seq) { - if (!ele) { + if (ele.empty()) { continue; } - if (!seen.insert(*ele).second) { - dups.insert(*ele); + if (!seen.insert(ele).second) { + dups.insert(ele); } } return dups; } -[[nodiscard]] bool -leg_labels_contain(OptionalLabels const& labels, OptionalLabel const& label) -{ - return std::ranges::find(labels, label) != labels.end(); -} - [[nodiscard]] std::string -format_leg_labels(OptionalLabels const& labels) +format_leg_labels(LegLabelList const& labels) { std::ostringstream out; out << '['; @@ -47,39 +43,42 @@ format_leg_labels(OptionalLabels const& labels) out << ", "; } first = false; - if (l) { - out << *l; + if (l.empty()) { + out << "''"; } else { - out << "None"; + out << l; } } out << ']'; return out.str(); } -[[nodiscard]] std::unordered_map -build_labelmap(OptionalLabels const& labels) +[[nodiscard]] std::string +format_duplicate_error(std::unordered_set const& dup) { - std::unordered_map map; - for (int64 i = 0; i < static_cast(labels.size()); ++i) { - if (labels[static_cast(i)]) { - map[*labels[static_cast(i)]] = i; + std::string joined; + bool first = true; + for (auto const& d : dup) { + if (!first) { + joined += ", "; } + first = false; + joined += d; } - return map; + return std::format("Duplicate leg labels: {}", joined); } } // namespace bool -is_valid_leg_label(OptionalLabel const& label) +is_valid_leg_label(std::string const& label) { - if (!label) { + if (label.empty()) { return true; } // TODO extend: check for valid syntax of combined / conjugated labels? for (char const* forbidden : FORBIDDEN_LEG_LABEL_CHARS) { - if (label->find(forbidden) != std::string::npos) { + if (label.find(forbidden) != std::string::npos) { return false; } } @@ -87,34 +86,34 @@ is_valid_leg_label(OptionalLabel const& label) } std::string -_combine_leg_labels(OptionalLabels const& labels, int64 offset) +_combine_leg_labels(LegLabelList const& labels, int64 offset) { std::string joined; for (std::size_t n = 0; n < labels.size(); ++n) { if (n > 0) { joined += '.'; } - if (!labels[n]) { + if (labels[n].empty()) { joined += std::format("?{}", static_cast(n) + offset); } else { - joined += *labels[n]; + joined += labels[n]; } } return std::format("({})", joined); } -OptionalLabels -_split_leg_label(OptionalLabel const& label, std::optional num) +LegLabelList +_split_leg_label(std::string const& label, std::optional num) { - if (!label) { + if (label.empty()) { if (!num.has_value()) { throw std::invalid_argument("num is required when splitting an unset combined label"); } - return OptionalLabels(static_cast(*num), std::nullopt); + return LegLabelList(static_cast(*num), ""); } - if (label->starts_with('(') && label->ends_with(')')) { - std::string const inner = label->substr(1, label->size() - 2); - OptionalLabels labels; + if (label.starts_with('(') && label.ends_with(')')) { + std::string const inner = label.substr(1, label.size() - 2); + LegLabelList labels; // Match Python ``str.split('.')`` (keeps empty segments). std::size_t start = 0; while (true) { @@ -122,7 +121,7 @@ _split_leg_label(OptionalLabel const& label, std::optional num) std::string part = (dot == std::string::npos) ? inner.substr(start) : inner.substr(start, dot - start); if (part.starts_with('?')) { - labels.push_back(std::nullopt); + labels.emplace_back(""); } else { labels.emplace_back(std::move(part)); } @@ -140,10 +139,10 @@ _split_leg_label(OptionalLabel const& label, std::optional num) throw std::invalid_argument("Invalid format for a combined label"); } -OptionalLabels -_dual_label_list(OptionalLabels const& labels) +LegLabelList +_dual_label_list(LegLabelList const& labels) { - OptionalLabels out; + LegLabelList out; out.reserve(labels.size()); for (auto const& l : labels | std::views::reverse) { out.push_back(_dual_leg_label(l)); @@ -151,38 +150,38 @@ _dual_label_list(OptionalLabels const& labels) return out; } -OptionalLabel -_dual_leg_label(OptionalLabel const& label) +std::string +_dual_leg_label(std::string const& label) { - if (!label) { - return std::nullopt; + if (label.empty()) { + return ""; } - if (label->starts_with('(') && label->ends_with(')')) { + if (label.starts_with('(') && label.ends_with(')')) { return _combine_leg_labels(_dual_label_list(_split_leg_label(label))); } - if (label->ends_with('*')) { - return label->substr(0, label->size() - 1); + if (label.ends_with('*')) { + return label.substr(0, label.size() - 1); } - return *label + '*'; + return label + '*'; } -OptionalLabels -_get_matching_labels(OptionalLabels const& labels1, OptionalLabels const& labels2) +LegLabelList +_get_matching_labels(LegLabelList const& labels1, LegLabelList const& labels2) { - OptionalLabels labels; + LegLabelList labels; std::vector conflicts; auto const n = std::min(labels1.size(), labels2.size()); labels.reserve(n); for (std::size_t i = 0; i < n; ++i) { auto const& l1 = labels1[i]; auto const& l2 = labels2[i]; - if (!l1) { + if (l1.empty()) { labels.push_back(l2); - } else if (!l2 || l1 == l2) { + } else if (l2.empty() || l1 == l2) { labels.push_back(l1); } else { conflicts.push_back(static_cast(i)); - labels.push_back(std::nullopt); + labels.emplace_back(""); } } if (!conflicts.empty() && Py_IsInitialized()) { @@ -198,236 +197,323 @@ _get_matching_labels(OptionalLabels const& labels1, OptionalLabels const& labels positions, format_leg_labels(labels1), format_leg_labels(labels2)); - // C++ debug log (was Python logging.getLogger(...).debug). std::cerr << "lasyten.tensors._tensors DEBUG: " << msg << '\n'; } return labels; } -LabelledLegs::LabelledLegs(OptionalLabels labels) +LegLabels::LegLabels(LegLabelList labels) { - auto const dup = duplicate_leg_labels(labels); - if (!dup.empty()) { - std::string joined; - bool first = true; - for (auto const& d : dup) { - if (!first) { - joined += ", "; - } - first = false; - joined += d; - } - throw std::invalid_argument(std::format("Duplicate leg labels: {}", joined)); + assign_impl(std::move(labels), /*require_same_size=*/false); +} + +LegLabels::LegLabels(LegLabels const& other) + : labels_(other.labels_) +{ + // Hooks are owner-specific; do not copy. +} + +LegLabels::LegLabels(LegLabels&& other) noexcept + : labels_(std::move(other.labels_)) +{ + other.before_hook_ = nullptr; + other.after_hook_ = nullptr; +} + +LegLabels& +LegLabels::operator=(LegLabels const& other) +{ + if (this == &other) { + return *this; } - num_legs = static_cast(labels.size()); - _labelmap = build_labelmap(labels); - _labels = std::move(labels); + // Keep our hooks; replace data (with validation). + return set(other.labels_); } -void -LabelledLegs::test_sanity() const +LegLabels& +LegLabels::operator=(LegLabels&& other) noexcept { - assert(std::ranges::all_of(_labels, [](auto const& l) { return is_valid_leg_label(l); })); - assert(duplicate_leg_labels(_labels).empty()); - std::unordered_set values; - for (auto const& [key, v] : _labelmap) { - (void)key; - assert(values.insert(v).second); + if (this == &other) { + return *this; } + // Keep our hooks; move data without re-running hooks (move assign of the container + // is only used when rebuilding owners that re-bind hooks afterward). + labels_ = std::move(other.labels_); + other.before_hook_ = nullptr; + other.after_hook_ = nullptr; + return *this; +} + +LegLabels& +LegLabels::operator=(LegLabelList labels) +{ + return set(std::move(labels)); } bool -LabelledLegs::is_fully_labelled() const +LegLabels::operator==(LegLabelList const& other) const { - return std::ranges::all_of(_labels, [](OptionalLabel const& l) { return l.has_value(); }); + return labels_ == other; } -OptionalLabels -LabelledLegs::labels() const +bool +LegLabels::operator==(LegLabels const& other) const { - return _labels; + return labels_ == other.labels_; } -std::vector -LabelledLegs::get_leg_idcs(int64 idx) const +void +LegLabels::set_hooks(BeforeHook before, AfterHook after) { - return { to_valid_idx(idx, num_legs) }; + before_hook_ = std::move(before); + after_hook_ = std::move(after); } -std::vector -LabelledLegs::get_leg_idcs(std::string const& label) const +void +LegLabels::clear_hooks() +{ + before_hook_ = nullptr; + after_hook_ = nullptr; +} + +void +LegLabels::test_sanity() const +{ + assert(std::ranges::all_of(labels_, [](auto const& l) { return is_valid_leg_label(l); })); + assert(duplicate_leg_labels(labels_).empty()); +} + +bool +LegLabels::is_fully_labelled() const +{ + return std::ranges::all_of(labels_, [](std::string const& l) { return !l.empty(); }); +} + +std::string const& +LegLabels::operator[](int64 idx) const +{ + return labels_[static_cast(index(idx))]; +} + +LegLabels::Ref +LegLabels::operator[](int64 idx) +{ + return Ref{ *this, idx }; +} + +LegLabels::Ref +LegLabels::operator[](std::string const& label) +{ + return Ref{ *this, label }; +} + +LegLabels::Ref& +LegLabels::Ref::operator=(std::string label) { - auto it = _labelmap.find(label); - if (it == _labelmap.end()) { + if (std::holds_alternative(key_)) { + self_.set_at(std::get(key_), std::move(label)); + } else { + self_.rename(std::get(key_), std::move(label)); + } + return *this; +} + +LegLabels::Ref:: +operator std::string const&() const +{ + if (std::holds_alternative(key_)) { + return self_[std::get(key_)]; + } + return self_[self_.index(std::get(key_))]; +} + +void +LegLabels::assign_impl(LegLabelList labels, bool require_same_size) +{ + auto const dups = duplicate_leg_labels(labels); + if (!dups.empty()) { + throw std::invalid_argument(format_duplicate_error(dups)); + } + if (require_same_size && !labels_.empty() && labels.size() != labels_.size()) { throw std::invalid_argument( - std::format("No leg with label {}. Labels are {}", label, format_leg_labels(_labels))); + std::format("expected {} labels, got {}", labels_.size(), labels.size())); } - return { it->second }; + for (auto const& l : labels) { + if (!is_valid_leg_label(l)) { + throw std::invalid_argument(std::format("Invalid leg label: '{}'", l)); + } + } + if (before_hook_) { + before_hook_(*this, labels); + } + labels_ = std::move(labels); + if (after_hook_) { + after_hook_(*this); + } +} + +LegLabels& +LegLabels::set(LegLabelList labels) +{ + assign_impl(std::move(labels), /*require_same_size=*/!labels_.empty()); + return *this; +} + +void +LegLabels::set_at(int64 pos, std::string label) +{ + pos = index(pos); + if (!is_valid_leg_label(label)) { + throw std::invalid_argument(std::format("Invalid leg label: '{}'", label)); + } + if (!label.empty()) { + for (std::size_t i = 0; i < labels_.size(); ++i) { + if (static_cast(i) == pos) { + continue; + } + if (labels_[i] == label) { + throw std::invalid_argument("Duplicate label"); + } + } + } + LegLabelList proposed = labels_; + proposed[static_cast(pos)] = label; + if (before_hook_) { + before_hook_(*this, proposed); + } + labels_ = std::move(proposed); + if (after_hook_) { + after_hook_(*this); + } +} + +void +LegLabels::rename(std::string const& old_label, std::string new_label) +{ + auto const pos = index(old_label); + set_at(pos, std::move(new_label)); +} + +LegLabels& +LegLabels::update(std::map const& mapping) +{ + LegLabelList next; + next.reserve(labels_.size()); + for (auto const& l : labels_) { + if (!l.empty()) { + auto it = mapping.find(l); + next.push_back(it == mapping.end() ? l : it->second); + } else { + next.emplace_back(""); + } + } + return set(std::move(next)); +} + +int64 +LegLabels::find_label(std::string const& label) const +{ + if (label.empty()) { + throw std::invalid_argument("Cannot look up an unset (empty) label"); + } + for (std::size_t i = 0; i < labels_.size(); ++i) { + if (labels_[i] == label) { + return static_cast(i); + } + } + throw std::invalid_argument( + std::format("No leg with label {}. Labels are {}", label, format_leg_labels(labels_))); +} + +int64 +LegLabels::index(int64 idx) const +{ + return to_valid_idx(idx, num_legs()); +} + +int64 +LegLabels::index(std::string const& label) const +{ + return find_label(label); } std::vector -LabelledLegs::get_leg_idcs(std::vector> const& idcs) const +LegLabels::indices(std::vector> const& idcs) const { std::vector res; res.reserve(idcs.size()); for (auto const& idx : idcs) { if (std::holds_alternative(idx)) { - auto const& label = std::get(idx); - auto it = _labelmap.find(label); - if (it == _labelmap.end()) { - throw std::invalid_argument(std::format( - "No leg with label {}. Labels are {}", label, format_leg_labels(_labels))); - } - res.push_back(it->second); + res.push_back(index(std::get(idx))); } else { - res.push_back(to_valid_idx(std::get(idx), num_legs)); + res.push_back(index(std::get(idx))); } } return res; } bool -LabelledLegs::has_label(std::string const& label) const +LegLabels::has(std::string const& label) const { - return leg_labels_contain(_labels, OptionalLabel{ label }); + if (label.empty()) { + return false; + } + return std::ranges::find(labels_, label) != labels_.end(); } bool -LabelledLegs::has_label(std::vector const& more) const +LegLabels::has(std::vector const& more) const { - return std::ranges::all_of(more, [this](std::string const& l) { - return leg_labels_contain(_labels, OptionalLabel{ l }); - }); + return std::ranges::all_of(more, [this](std::string const& l) { return has(l); }); } bool -LabelledLegs::labels_are(std::vector const& want, bool planar) const +LegLabels::are(std::vector const& want, bool planar) const { if (!is_fully_labelled()) { return false; } - if (static_cast(want.size()) != num_legs) { + if (static_cast(want.size()) != num_legs()) { return false; } if (planar) { if (want.empty()) { return true; } - auto const start = _labelmap.find(want.front()); - if (start == _labelmap.end()) { + int64 start = -1; + try { + start = find_label(want.front()); + } catch (std::invalid_argument const&) { return false; } for (std::size_t i = 1; i < want.size(); ++i) { - auto const idx = (static_cast(start->second) + i) % want.size(); - if (*_labels[idx] != want[i]) { + auto const idx = (static_cast(start) + i) % want.size(); + if (labels_[idx] != want[i]) { return false; } } return true; } - // Equal-length lists with unique labels can be compared as sets. std::set a(want.begin(), want.end()); - std::set b; - for (auto const& l : _labels) { - b.insert(*l); - } + std::set b(labels_.begin(), labels_.end()); return a == b; } -LabelledLegs& -LabelledLegs::relabel(std::map const& mapping) +void +LegLabels::save_hdf5(lasyten::hdf5::Saver& saver, + HighFive::Group& /*h5gr*/, + std::string const& subpath) const { - OptionalLabels next; - next.reserve(_labels.size()); - for (auto const& l : _labels) { - if (l) { - auto it = mapping.find(*l); - next.push_back(it == mapping.end() ? l : OptionalLabel{ it->second }); - } else { - next.push_back(std::nullopt); - } - } - return set_labels(std::move(next)); + hdf5_export::save_leg_labels(saver, subpath + "labels", labels_); } -LabelledLegs& -LabelledLegs::set_label(int64 pos, OptionalLabel label) +LegLabels +LegLabels::from_hdf5(lasyten::hdf5::Loader& loader, + HighFive::Group& /*h5gr*/, + std::string const& subpath) { - pos = to_valid_idx(pos, num_legs); - auto const p = static_cast(pos); - for (std::size_t i = 0; i < p; ++i) { - if (_labels[i] == label) { - throw std::invalid_argument("Duplicate label"); - } - } - for (std::size_t i = p + 1; i < _labels.size(); ++i) { - if (_labels[i] == label) { - throw std::invalid_argument("Duplicate label"); - } - } - if (_labels[p]) { - _labelmap.erase(*_labels[p]); - } - _labels[p] = label; - if (label) { - _labelmap[*label] = pos; - } - return *this; -} - -LabelledLegs& -LabelledLegs::set_labels(OptionalLabels labels) -{ - auto const dups = duplicate_leg_labels(labels); - if (!dups.empty()) { - std::string joined; - bool first = true; - for (auto const& d : dups) { - if (!first) { - joined += ", "; - } - first = false; - joined += d; - } - throw std::invalid_argument(std::format("Duplicate leg labels: {}", joined)); - } - if (static_cast(labels.size()) != num_legs) { - throw std::invalid_argument( - std::format("expected {} labels, got {}", num_legs, labels.size())); - } - _labels = std::move(labels); - _labelmap = build_labelmap(_labels); - return *this; + return LegLabels(hdf5_export::load_leg_labels(loader, subpath + "labels")); } } // namespace lasyten - -// ============================================================================= -// ORPHANED PYTHON COMMENT HINTS (no matching C++ function body found) -// ============================================================================= -// --- DiagonalTensor.__rsub__ --- -// other - self -// --- DiagonalTensor.__sub__ --- -// other - self -// --- Mask.__and__ --- -// ``self & other`` -// --- Mask.__eq__ --- -// ``self == other`` -// --- Mask.__invert__ --- -// ``~self`` -// --- Mask.__ne__ --- -// ``self != other`` -// --- Mask.__or__ --- -// ``self | other`` -// --- Mask.__rand__ --- -// ``other & self`` -// --- Mask.__ror__ --- -// ``other | self`` -// --- Mask.__rxor__ --- -// ``other ^ self`` -// --- Mask.__xor__ --- -// ``self ^ other`` -// --- _elementwise_function --- -// kwargs take precedence over func_kwargs -// ============================================================================= diff --git a/src/tensors/mask.cpp b/src/tensors/mask.cpp index 60486232..b10f499b 100644 --- a/src/tensors/mask.cpp +++ b/src/tensors/mask.cpp @@ -122,7 +122,7 @@ make_mask(TensorBackend::DataPtr data, Space::Ptr space_out, bool is_projection, TensorBackend::Ptr backend, - std::optional labels) + std::optional labels) { as_elementary_space(space_in); as_elementary_space(space_out); @@ -161,7 +161,7 @@ Mask::Mask(TensorBackend::DataPtr data_in, bool is_projection_in, TensorBackend::Ptr backend_in, Symmetry::Ptr symmetry_in, - OptionalLabels labels_in, + LegLabelList labels_in, std::string device_in) : Tensor(std::make_shared( std::vector{ std::dynamic_pointer_cast(space_out) }), @@ -311,7 +311,7 @@ Mask::Ptr Mask::from_eye(Space::Ptr leg, bool is_projection_flag, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional device) { auto diag = DiagonalTensor::from_eye(std::move(leg), backend, labels, Dtype::Bool, device); @@ -326,7 +326,7 @@ Mask::Ptr Mask::from_block_mask(BlockBackend::BlockPtr block_mask, Space::Ptr large_leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional device) { if (!large_leg->symmetry->can_be_dropped()) { @@ -358,7 +358,7 @@ Mask::from_DiagonalTensor(DiagonalTensorCPtr diag) true, diag->backend, diag->symmetry, - diag->labels(), + diag->labels.as_list(), diag->device); } @@ -366,7 +366,7 @@ Mask::Ptr Mask::from_indices(py::object indices, Space::Ptr large_leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional device) { backend = resolve_backend(std::move(backend), large_leg); @@ -394,7 +394,7 @@ Mask::from_random(Space::Ptr large_leg_in, TensorBackend::Ptr backend, float64 p_keep, int64 min_keep, - std::optional labels, + std::optional labels, std::optional device, py::object np_random) { @@ -511,7 +511,7 @@ Mask::from_random(Space::Ptr large_leg_in, Mask::Ptr Mask::from_zero(Space::Ptr large_leg, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional device) { backend = resolve_backend(std::move(backend), large_leg); @@ -545,7 +545,7 @@ Mask::as_DiagonalTensor(Dtype out_dtype) large_leg(), backend, symmetry, - labels()); + labels.as_list()); } SymmetricTensorPtr @@ -580,7 +580,7 @@ Mask::as_SymmetricTensor(bool /*guarantee_copy*/, auto new_data = backend->full_data_from_mask( std::static_pointer_cast(shared_from_this()), out_dtype); return std::make_shared( - new_data, codomain, domain, backend, symmetry, labels()); + new_data, codomain, domain, backend, symmetry, labels.as_list()); } Mask::Ptr @@ -618,7 +618,7 @@ Mask::_binary_operand(MaskCPtr other, BlockBinaryFn func, std::string const& ope } auto [data_out, small] = same->mask_binary_operand( std::static_pointer_cast(shared_from_this()), other, std::move(func)); - auto labs = _get_matching_labels(labels(), other->labels()); + auto labs = _get_matching_labels(labels.as_list(), other->labels.as_list()); return make_mask(data_out, large_leg(), small, is_projection, same, labs); } @@ -638,7 +638,7 @@ Mask::_unary_operand(BlockUnaryFn func) auto [data_out, small] = backend->mask_unary_operand(std::static_pointer_cast(shared_from_this()), func); - return make_mask(data_out, large_leg(), small, true, backend, labels()); + return make_mask(data_out, large_leg(), small, true, backend, labels.as_list()); } Tensor::Ptr @@ -668,15 +668,15 @@ Mask::copy(bool deep, std::optional device_opt, std::optionalget_device_from_data(new_data)); } Tensor::Ptr Mask::dagger() const { - auto labs = labels(); - OptionalLabels dual_rev; + auto labs = labels.as_list(); + LegLabelList dual_rev; dual_rev.reserve(labs.size()); for (auto it = labs.rbegin(); it != labs.rend(); ++it) { dual_rev.push_back(_dual_leg_label(*it)); @@ -800,7 +800,7 @@ Mask::to_backend(TensorBackend::Ptr new_backend, is_projection, new_backend, symmetry, - labels(), + labels.as_list(), device_s); } @@ -858,7 +858,7 @@ Mask::to_dense_block(std::optional> } } if (leg_order.has_value()) { - auto idcs = get_leg_idcs(*leg_order); + auto idcs = labels.indices(*leg_order); res = bb->permute_axes(res, idcs); } return res; @@ -896,7 +896,7 @@ Mask::save_hdf5(lasyten::hdf5::Saver& saver, hdf5_io::h5_set_attr(h5gr.getId(), "num_legs", static_cast(num_legs)); hdf5_export::save_f64_vector(saver, subpath + "shape", shape); hdf5_io::h5_set_attr(h5gr.getId(), "is_projection", is_projection); - hdf5_export::save_optional_labels(saver, subpath + "labels", _labels); + hdf5_export::save_leg_labels(saver, subpath + "labels", labels.as_list()); } Mask::Ptr @@ -919,11 +919,11 @@ Mask::from_hdf5(lasyten::hdf5::Loader& loader, HighFive::Group& h5gr, std::strin proj = space_dim(*space_in) >= space_dim(*space_out); } - OptionalLabels labels_in(2, std::nullopt); + LegLabelList labels_in(2, ""); if (hdf5_io::h5_contains(loader.root(), subpath + "labels")) { - labels_in = hdf5_export::load_optional_labels(loader, subpath + "labels"); + labels_in = hdf5_export::load_leg_labels(loader, subpath + "labels"); if (labels_in.empty()) { - labels_in.assign(2, std::nullopt); + labels_in.assign(2, ""); } } diff --git a/src/tensors/ops_algebra.cpp b/src/tensors/ops_algebra.cpp index 1d5afd7d..8c128b7d 100644 --- a/src/tensors/ops_algebra.cpp +++ b/src/tensors/ops_algebra.cpp @@ -81,7 +81,7 @@ maybe_wrap_hidden(TensorPtr result, bool wrap_if_hidden_labels) return result; } if (as_Diagonal(result) || as_Mask(result) || as_Identity(result)) { - if (HiddenLegTensor::has_hidden_leg_labels(result->labels())) { + if (HiddenLegTensor::has_hidden_leg_labels(result->labels.as_list())) { throw std::runtime_error( "Internal error: DiagonalTensor/Mask/Identity with hidden labels"); } @@ -120,7 +120,7 @@ reject_hidden_leg_arguments(TensorCPtr tensor, std::vector const& leg_idc if (!as_Hidden(tensor)) { return; } - auto const& labs = tensor->labels(); + auto const& labs = tensor->labels.as_list(); for (auto idx : leg_idcs) { if (idx < 0) { idx += tensor->num_legs; @@ -129,12 +129,13 @@ reject_hidden_leg_arguments(TensorCPtr tensor, std::vector const& leg_idc continue; } if (HiddenLegTensor::is_hidden_leg_label(labs[static_cast(idx)])) { - throw std::invalid_argument( - std::format("{}: cannot specify hidden leg '{}' (index {}) in arguments. " - "Hidden legs are handled implicitly.", - op, - labs[static_cast(idx)].value_or("?"), - idx)); + throw std::invalid_argument(std::format( + "{}: cannot specify hidden leg '{}' (index {}) in arguments. " + "Hidden legs are handled implicitly.", + op, + labs[static_cast(idx)].empty() ? "?" + : labs[static_cast(idx)], + idx)); } } } @@ -148,23 +149,23 @@ implicit_hidden_contraction_pairs(TensorCPtr tensor1, TensorCPtr tensor2) if (!h1 || !h2) { return pairs; } - auto const& labs1 = tensor1->labels(); - auto const& labs2 = tensor2->labels(); + auto const& labs1 = tensor1->labels.as_list(); + auto const& labs2 = tensor2->labels.as_list(); std::vector> hidden1; std::vector> hidden2; for (int64 i = 0; i < tensor1->num_legs; ++i) { if (HiddenLegTensor::is_hidden_leg_label(labs1[static_cast(i)])) { - hidden1.emplace_back(i, *labs1[static_cast(i)]); + hidden1.emplace_back(i, labs1[static_cast(i)]); } } for (int64 i = 0; i < tensor2->num_legs; ++i) { if (HiddenLegTensor::is_hidden_leg_label(labs2[static_cast(i)])) { - hidden2.emplace_back(i, *labs2[static_cast(i)]); + hidden2.emplace_back(i, labs2[static_cast(i)]); } } std::vector used2(hidden2.size(), false); for (auto const& [i1, lab1] : hidden1) { - auto dual1 = _dual_leg_label(OptionalLabel{ lab1 }); + auto dual1 = _dual_leg_label(lab1); for (std::size_t j = 0; j < hidden2.size(); ++j) { if (used2[j]) { continue; @@ -177,7 +178,7 @@ implicit_hidden_contraction_pairs(TensorCPtr tensor1, TensorCPtr tensor2) "implicitly.", lab1)); } - if (dual1 && *dual1 == lab2) { + if (dual1 == lab2) { pairs.emplace_back(i1, i2); used2[j] = true; break; @@ -225,21 +226,21 @@ leg_refs_from_ints(std::vector const& idcs) return out; } -OptionalLabel -relabel_one(OptionalLabel lab, std::optional> const& relabel) +std::string +relabel_one(std::string lab, std::optional> const& relabel) { - if (!lab.has_value() || !relabel.has_value()) { + if (lab.empty() || !relabel.has_value()) { return lab; } - auto it = relabel->find(*lab); + auto it = relabel->find(lab); if (it != relabel->end()) { return it->second; } return lab; } -OptionalLabels -apply_relabel(OptionalLabels labels, +LegLabelList +apply_relabel(LegLabelList labels, std::optional> const& relabel) { if (!relabel.has_value()) { @@ -251,10 +252,10 @@ apply_relabel(OptionalLabels labels, return labels; } -OptionalLabels -dual_labels_reversed(OptionalLabels const& labs) +LegLabelList +dual_labels_reversed(LegLabelList const& labs) { - OptionalLabels dual_labs; + LegLabelList dual_labs; for (auto it = labs.rbegin(); it != labs.rend(); ++it) { dual_labs.push_back(_dual_leg_label(*it)); } @@ -288,7 +289,7 @@ make_mask_native(TensorBackend::DataPtr data, Space::Ptr space_out, bool is_projection, TensorBackend::Ptr backend, - OptionalLabels labels) + LegLabelList labels) { return std::make_shared(std::move(data), std::move(space_in), @@ -305,7 +306,7 @@ make_symmetric_native(TensorBackend::DataPtr data, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - OptionalLabels labels) + LegLabelList labels) { return std::make_shared(std::move(data), std::move(codomain), @@ -319,7 +320,7 @@ make_symmetric_native(TensorBackend::DataPtr data, make_diagonal_native(TensorBackend::DataPtr data, Space::Ptr leg, TensorBackend::Ptr backend, - OptionalLabels labels) + LegLabelList labels) { return std::make_shared( std::move(data), std::move(leg), std::move(backend), leg->symmetry, std::move(labels)); @@ -475,14 +476,14 @@ charge_leg_label() return ChargedTensor::_CHARGE_LEG_LABEL; } -[[nodiscard]] OptionalLabels +[[nodiscard]] LegLabelList labels_from_tensor(TensorCPtr t) { - return t->labels(); + return t->labels.as_list(); } -[[nodiscard]] OptionalLabels -identity_labels(OptionalLabels labels) +[[nodiscard]] LegLabelList +identity_labels(LegLabelList labels) { return labels; } @@ -500,7 +501,7 @@ backend_item(TensorCPtr t) } [[nodiscard]] IdentityPtr -make_identity_native(Space::Ptr leg, TensorBackend::Ptr backend, OptionalLabels labels) +make_identity_native(Space::Ptr leg, TensorBackend::Ptr backend, LegLabelList labels) { auto dt = SymmetricTensor::_parse_default_dtype(std::nullopt, leg->symmetry); if (!dt.has_value()) { @@ -515,10 +516,10 @@ make_identity_native(Space::Ptr leg, TensorBackend::Ptr backend, OptionalLabels std::move(device_s)); } -OptionalLabels -nested_flat_labels(OptionalLabels codomain_labels, OptionalLabels domain_labels) +LegLabelList +nested_flat_labels(LegLabelList codomain_labels, LegLabelList domain_labels) { - OptionalLabels res = std::move(codomain_labels); + LegLabelList res = std::move(codomain_labels); for (auto it = domain_labels.rbegin(); it != domain_labels.rend(); ++it) { res.push_back(*it); } @@ -549,7 +550,7 @@ splice_tp_factors(TensorProduct::Ptr const& base, [[nodiscard]] int64 leg_ref_index(TensorCPtr tensor, LegRef const& leg) { - return std::visit([&](auto const& x) -> int64 { return tensor->get_leg_idcs(x).at(0); }, leg); + return std::visit([&](auto const& x) -> int64 { return tensor->labels.index(x); }, leg); } [[nodiscard]] std::optional @@ -765,11 +766,11 @@ space_factor0(TensorProduct::Ptr const& tp) } [[nodiscard]] TensorPtr -shallow_copy_labels(TensorCPtr t, OptionalLabels labels) +shallow_copy_labels(TensorCPtr t, LegLabelList labels) { TensorPtr mut = std::const_pointer_cast(t); TensorPtr res = mut->copy(/*deep=*/false); - res->set_labels(std::move(labels)); + res->labels.set(std::move(labels)); return res; } @@ -790,7 +791,7 @@ dagger(TensorCPtr tensor) if (auto m = as_Mask(tensor)) { auto backend = m->backend; auto data = backend->mask_dagger(m); - OptionalLabels dual_labs = dual_labels_reversed(m->labels()); + LegLabelList dual_labs = dual_labels_reversed(m->labels.as_list()); return make_mask_native(std::move(data), space_factor0(m->codomain), space_factor0(m->domain), @@ -802,12 +803,12 @@ dagger(TensorCPtr tensor) return std::const_pointer_cast(tensor); } if (auto d = as_Diagonal(tensor)) { - OptionalLabels dual_labs = dual_labels_reversed(d->labels()); + LegLabelList dual_labs = dual_labels_reversed(d->labels.as_list()); if (d->dtype == Dtype::Bool) { return shallow_copy_labels(d, std::move(dual_labs)); } DiagonalTensorPtr res = complex_conj(std::const_pointer_cast(d)); - res->set_labels(std::move(dual_labs)); + res->labels.set(std::move(dual_labs)); return res; } if (auto h = as_Hidden(tensor)) { @@ -816,14 +817,14 @@ dagger(TensorCPtr tensor) if (auto s = as_Symmetric(tensor)) { auto backend = s->backend; auto data = backend->dagger(s); - OptionalLabels dual_labs = dual_labels_reversed(s->labels()); + LegLabelList dual_labs = dual_labels_reversed(s->labels.as_list()); return make_symmetric_native( std::move(data), s->domain, s->codomain, backend, std::move(dual_labs)); } if (auto c = as_Charged(tensor)) { SymmetricTensorPtr inv_part = std::dynamic_pointer_cast(dagger(c->invariant_part)); - inv_part->set_label(0, charge_leg_label()); + inv_part->labels[0] = charge_leg_label(); inv_part = std::dynamic_pointer_cast( move_leg_wrap(inv_part, 0, std::nullopt, 0, BendRight{ true })); auto charged_state = c->backend->block_backend->conj(c->charged_state); @@ -835,10 +836,10 @@ dagger(TensorCPtr tensor) TensorPtr transpose(TensorCPtr tensor) { - OptionalLabels labels; + LegLabelList labels; { - OptionalLabels domain_labels = tensor->domain_labels(); - OptionalLabels codomain_labels = tensor->codomain_labels(); + LegLabelList domain_labels = tensor->domain_labels(); + LegLabelList codomain_labels = tensor->codomain_labels(); for (auto it = domain_labels.rbegin(); it != domain_labels.rend(); ++it) { labels.push_back(*it); } @@ -1067,7 +1068,8 @@ scalar_multiply(BlockBackend::Scalar const& a, TensorCPtr v) auto backend = h->backend; auto data = backend->mul(a, h); return maybe_wrap_hidden( - make_symmetric_native(std::move(data), h->codomain, h->domain, backend, h->labels()), + make_symmetric_native( + std::move(data), h->codomain, h->domain, backend, h->labels.as_list()), true); } auto s = as_Symmetric(v); @@ -1076,7 +1078,8 @@ scalar_multiply(BlockBackend::Scalar const& a, TensorCPtr v) } auto backend = s->backend; auto data = backend->mul(a, s); - return make_symmetric_native(std::move(data), s->codomain, s->domain, backend, s->labels()); + return make_symmetric_native( + std::move(data), s->codomain, s->domain, backend, s->labels.as_list()); } TensorPtr @@ -1132,7 +1135,7 @@ linear_combination(BlockBackend::Scalar const& a, std::static_pointer_cast(hv_mut->as_SymmetricTensor()), b, std::static_pointer_cast(hw_mut->as_SymmetricTensor()))); - res->set_labels(hv->labels()); + res->labels.set(hv->labels.as_list()); return maybe_wrap_hidden(res, true); } } @@ -1148,7 +1151,7 @@ linear_combination(BlockBackend::Scalar const& a, } auto backend = get_same_backend({ v, w }); auto data = backend->linear_combination(a, vs, b, ws); - OptionalLabels labels = _get_matching_labels(vs->labels(), ws->labels()); + LegLabelList labels = _get_matching_labels(vs->labels.as_list(), ws->labels.as_list()); return make_symmetric_native( std::move(data), vs->codomain, vs->domain, backend, std::move(labels)); } @@ -1184,7 +1187,7 @@ scale_axis(TensorCPtr tensor, DiagonalTensorCPtr diag, LegRef leg) BlockBackend::BlockPtr const& y) { return (*x) * (*y); }; auto prod = dt_mut->_binary_operand( std::const_pointer_cast(diag_use), std::move(mul_fn), "*"); - prod->set_labels(tensor->labels()); + prod->labels.set(tensor->labels.as_list()); return prod; } if (auto m = as_Mask(tensor)) { @@ -1194,7 +1197,7 @@ scale_axis(TensorCPtr tensor, DiagonalTensorCPtr diag, LegRef leg) } else { res = std::get(compose(m, diag_use)); } - res->set_labels(tensor->labels()); + res->labels.set(tensor->labels.as_list()); return res; } if (auto c = as_Charged(tensor)) { @@ -1206,7 +1209,7 @@ scale_axis(TensorCPtr tensor, DiagonalTensorCPtr diag, LegRef leg) auto data = backend->scale_axis(tensor, diag_use, leg_idx); // the labels of a HiddenLegTensor keep the hidden prefix -> wrap the result again return HiddenLegTensor::maybe_wrap(make_symmetric_native( - std::move(data), tensor->codomain, tensor->domain, backend, tensor->labels())); + std::move(data), tensor->codomain, tensor->domain, backend, tensor->labels.as_list())); } [[nodiscard]] TensorPtr @@ -1227,18 +1230,18 @@ compose(TensorCPtr tensor1, (void)get_same_device({ tensor1, tensor2 }); check_spaces_tp(tensor1->domain, tensor2->codomain); - OptionalLabels codomain_labels = apply_relabel(tensor1->codomain_labels(), relabel1); - OptionalLabels domain_labels = apply_relabel(tensor2->domain_labels(), relabel2); - OptionalLabels res_labels = nested_flat_labels(codomain_labels, domain_labels); + LegLabelList codomain_labels = apply_relabel(tensor1->codomain_labels(), relabel1); + LegLabelList domain_labels = apply_relabel(tensor2->domain_labels(), relabel2); + LegLabelList res_labels = nested_flat_labels(codomain_labels, domain_labels); if (auto m1 = as_Mask(tensor1)) { TensorPtr res = _compose_with_Mask(tensor2, std::const_pointer_cast(m1), 0); - res->set_label(0, m1->labels().at(0)); + res->labels[0] = m1->labels[0]; return maybe_wrap_hidden(std::move(res), true); } if (auto m2 = as_Mask(tensor2)) { TensorPtr res = _compose_with_Mask(tensor1, std::const_pointer_cast(m2), -1); - res->set_label(-1, m2->labels().at(1)); + res->labels[-1] = m2->labels[1]; return maybe_wrap_hidden(std::move(res), true); } if (as_Identity(tensor1)) { @@ -1252,7 +1255,7 @@ compose(TensorCPtr tensor1, scale_axis(tensor2, std::dynamic_pointer_cast(as_Diagonal(tensor1)), int64{ 0 }); - res->set_labels(res_labels); + res->labels.set(res_labels); return maybe_wrap_hidden(std::move(res), true); } if (as_Diagonal(tensor2)) { @@ -1260,7 +1263,7 @@ compose(TensorCPtr tensor1, scale_axis(tensor1, std::dynamic_pointer_cast(as_Diagonal(tensor2)), int64{ -1 }); - res->set_labels(res_labels); + res->labels.set(res_labels); return maybe_wrap_hidden(std::move(res), true); } if (as_Charged(tensor1)) { @@ -1349,8 +1352,8 @@ partial_compose(TensorCPtr tensor1, (void)get_same_device({ tensor1, tensor2 }); int64 t1_first = leg_ref_index(tensor1, tensor1_first_leg); - OptionalLabels codomain_labels = apply_relabel(tensor1->codomain_labels(), relabel1); - OptionalLabels domain_labels = apply_relabel(tensor1->domain_labels(), relabel1); + LegLabelList codomain_labels = apply_relabel(tensor1->codomain_labels(), relabel1); + LegLabelList domain_labels = apply_relabel(tensor1->domain_labels(), relabel1); char const* leg_msg = "Not all legs to be contracted are in the (co)domain"; char const* compose_msg = "Use compose for contracting the full (co)domain"; @@ -1375,7 +1378,7 @@ partial_compose(TensorCPtr tensor1, std::vector factors1(tensor1->codomain->factors.begin() + t1_first, tensor1->codomain->factors.begin() + t1_last + 1); check_leg_vectors(factors1, tensor2->domain->factors); - OptionalLabels tensor2_labels = apply_relabel(tensor2->codomain_labels(), relabel2); + LegLabelList tensor2_labels = apply_relabel(tensor2->codomain_labels(), relabel2); codomain_labels.erase(codomain_labels.begin() + t1_first, codomain_labels.begin() + t1_last + 1); codomain_labels.insert( @@ -1407,7 +1410,7 @@ partial_compose(TensorCPtr tensor1, std::vector factors1(tensor1->domain->factors.begin() + domain_first_leg, tensor1->domain->factors.begin() + domain_last_leg + 1); check_leg_vectors(factors1, tensor2->codomain->factors); - OptionalLabels tensor2_labels = apply_relabel(tensor2->domain_labels(), relabel2); + LegLabelList tensor2_labels = apply_relabel(tensor2->domain_labels(), relabel2); domain_labels.erase(domain_labels.begin() + domain_first_leg, domain_labels.begin() + domain_last_leg + 1); domain_labels.insert( @@ -1424,15 +1427,15 @@ partial_compose(TensorCPtr tensor1, std::make_shared(std::move(new_dom_factors), tensor1->symmetry); } - OptionalLabels res_labels = codomain_labels; + LegLabelList res_labels = codomain_labels; for (auto it = domain_labels.rbegin(); it != domain_labels.rend(); ++it) { res_labels.push_back(*it); } { std::vector named; for (auto const& lab : res_labels) { - if (lab.has_value()) { - named.push_back(*lab); + if (!lab.empty()) { + named.push_back(lab); } } if (!duplicate_entries(named).empty()) { @@ -1448,12 +1451,12 @@ partial_compose(TensorCPtr tensor1, } if (auto m2 = as_Mask(tensor2)) { TensorPtr res = _compose_with_Mask(tensor1, std::const_pointer_cast(m2), t1_first); - res->set_labels(res_labels); + res->labels.set(res_labels); return maybe_wrap_hidden(std::move(res), true); } if (auto d2 = as_Diagonal(tensor2)) { TensorPtr res = scale_axis(tensor1, d2, t1_first); - res->set_labels(res_labels); + res->labels.set(res_labels); return maybe_wrap_hidden(std::move(res), true); } @@ -1543,15 +1546,15 @@ outer(TensorCPtr tensor1, auto codomain = TensorProduct::from_partial_products({ s1->codomain, s2->codomain }); auto domain = TensorProduct::from_partial_products({ s1->domain, s2->domain }); - OptionalLabels codomain_labels = apply_relabel(s1->codomain_labels(), relabel1); - OptionalLabels domain_labels = apply_relabel(s1->domain_labels(), relabel1); + LegLabelList codomain_labels = apply_relabel(s1->codomain_labels(), relabel1); + LegLabelList domain_labels = apply_relabel(s1->domain_labels(), relabel1); { auto c2 = apply_relabel(s2->codomain_labels(), relabel2); auto d2 = apply_relabel(s2->domain_labels(), relabel2); codomain_labels.insert(codomain_labels.end(), c2.begin(), c2.end()); domain_labels.insert(domain_labels.end(), d2.begin(), d2.end()); } - OptionalLabels flat = nested_flat_labels(codomain_labels, domain_labels); + LegLabelList flat = nested_flat_labels(codomain_labels, domain_labels); return maybe_wrap_hidden( make_symmetric_native(std::move(data), codomain, domain, backend, flat), as_Hidden(t1) || as_Hidden(t2)); @@ -1661,8 +1664,8 @@ partial_trace(TensorCPtr tensor, traced_set.insert(i1); traced_set.insert(i2); } - OptionalLabels labels; - OptionalLabels all_labels = s->labels(); + LegLabelList labels; + LegLabelList all_labels = s->labels.as_list(); for (std::size_t n = 0; n < all_labels.size(); ++n) { if (!traced_set.contains(static_cast(n))) { labels.push_back(all_labels[n]); @@ -1683,8 +1686,8 @@ tdot(TensorCPtr tensor1, { (void)get_same_device({ tensor1, tensor2 }); - std::vector legs1_v = tensor1->get_leg_idcs(legs1); - std::vector legs2_v = tensor2->get_leg_idcs(legs2); + std::vector legs1_v = tensor1->labels.indices(legs1); + std::vector legs2_v = tensor2->labels.indices(legs2); if (!duplicate_entries(legs1_v).empty() || !duplicate_entries(legs2_v).empty()) { throw std::invalid_argument("Duplicate leg entries."); } @@ -1711,15 +1714,15 @@ tdot(TensorCPtr tensor1, skip1.insert(i1); skip2.insert(i2); } - OptionalLabels codomain_labels; - OptionalLabels all1 = tensor1->labels(); + LegLabelList codomain_labels; + LegLabelList all1 = tensor1->labels.as_list(); for (std::size_t n = 0; n < all1.size(); ++n) { if (!skip1.contains(static_cast(n))) { codomain_labels.push_back(relabel_one(all1[n], relabel1)); } } - OptionalLabels domain_labels; - OptionalLabels all2 = tensor2->labels(); + LegLabelList domain_labels; + LegLabelList all2 = tensor2->labels.as_list(); for (std::size_t n = 0; n < all2.size(); ++n) { if (!skip2.contains(static_cast(n))) { domain_labels.push_back(relabel_one(all2[n], relabel2)); @@ -1729,9 +1732,9 @@ tdot(TensorCPtr tensor1, if (std::holds_alternative(res)) { return res; } - OptionalLabels flat = codomain_labels; + LegLabelList flat = codomain_labels; flat.insert(flat.end(), domain_labels.begin(), domain_labels.end()); - std::get(res)->set_labels(flat); + std::get(res)->labels.set(flat); return res; } @@ -1760,7 +1763,7 @@ tdot(TensorCPtr tensor1, mask_use = std::dynamic_pointer_cast(transpose(m1)); } TensorPtr res = _compose_with_Mask(work2, mask_use, legs2_v[0]); - res->set_label(legs2_v[0], m1->labels()[static_cast(1 - legs1_v[0])]); + res->labels[legs2_v[0]] = m1->labels[static_cast(1 - legs1_v[0])]; try { return permute_legs_wrap(res, leg_refs_from_ints(legs1_v)); } catch (...) { @@ -1795,7 +1798,7 @@ tdot(TensorCPtr tensor1, mask_use = std::dynamic_pointer_cast(transpose(m2)); } TensorPtr res = _compose_with_Mask(work1, mask_use, legs1_v[0]); - res->set_label(legs1_v[0], m2->labels()[static_cast(1 - legs2_v[0])]); + res->labels[legs1_v[0]] = m2->labels[static_cast(1 - legs2_v[0])]; try { return permute_legs_wrap(res, std::nullopt, leg_refs_from_ints(legs2_v)); } catch (...) { @@ -1824,7 +1827,7 @@ tdot(TensorCPtr tensor1, if (auto id1 = as_Identity(work1)) { if (num_contr == 1) { TensorPtr res = permute_legs_wrap(work2, leg_refs_from_ints(legs2_v)); - res->set_label(0, id1->labels()[static_cast(1 - legs1_v[0])]); + res->labels[0] = id1->labels[static_cast(1 - legs1_v[0])]; return res; } if (num_contr == 2) { @@ -1839,7 +1842,7 @@ tdot(TensorCPtr tensor1, // Match Python (computes permute then ignores it): (void)permute_legs_wrap(work1, std::nullopt, leg_refs_from_ints(legs1_v)); TensorPtr res = work1->copy(/*deep=*/false); - res->set_label(legs1_v[0], id2->labels()[static_cast(1 - legs2_v[0])]); + res->labels[legs1_v[0]] = id2->labels[static_cast(1 - legs2_v[0])]; return res; } if (num_contr == 2) { @@ -1856,7 +1859,7 @@ tdot(TensorCPtr tensor1, work1 = std::const_pointer_cast(d1)->as_SymmetricTensor(); } else if (num_contr == 1) { TensorPtr res = scale_axis(work2, d1, legs2_v[0]); - res->set_label(legs2_v[0], d1->labels()[static_cast(1 - legs1_v[0])]); + res->labels[legs2_v[0]] = d1->labels[static_cast(1 - legs1_v[0])]; try { return permute_legs_wrap(res, leg_refs_from_ints(legs1_v)); } catch (...) { @@ -1876,7 +1879,7 @@ tdot(TensorCPtr tensor1, work2 = std::const_pointer_cast(d2)->as_SymmetricTensor(); } else if (num_contr == 1) { TensorPtr res = scale_axis(work1, d2, legs1_v[0]); - res->set_label(legs1_v[0], d2->labels()[static_cast(1 - legs2_v[0])]); + res->labels[legs1_v[0]] = d2->labels[static_cast(1 - legs2_v[0])]; try { return permute_legs_wrap(res, std::nullopt, leg_refs_from_ints(legs1_v)); } catch (...) { diff --git a/src/tensors/ops_elementwise.cpp b/src/tensors/ops_elementwise.cpp index 6e0fa665..64eca256 100644 --- a/src/tensors/ops_elementwise.cpp +++ b/src/tensors/ops_elementwise.cpp @@ -206,8 +206,12 @@ exp(TensorCPtr obj) sym, [bb](BlockBackend::BlockPtr const& block) { return bb->matrix_exp(block); }, std::nullopt); - auto res = std::make_shared( - std::move(data), sym->codomain, sym->domain, backend, sym->symmetry, sym->labels()); + auto res = std::make_shared(std::move(data), + sym->codomain, + sym->domain, + backend, + sym->symmetry, + sym->labels.as_list()); if (combine) { return split_legs(res, std::vector{ int64{ 0 }, int64{ 1 } }); } diff --git a/src/tensors/ops_legs.cpp b/src/tensors/ops_legs.cpp index b11868e7..b1902186 100644 --- a/src/tensors/ops_legs.cpp +++ b/src/tensors/ops_legs.cpp @@ -68,7 +68,7 @@ make_symmetric_native(TensorBackend::DataPtr data, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - OptionalLabels labels) + LegLabelList labels) { return std::make_shared(std::move(data), std::move(codomain), @@ -118,7 +118,7 @@ contains_int(std::vector const& v, int64 x) resolve_leg_refs(TensorCPtr const& tensor, std::vector const& refs) { std::vector> keys(refs.begin(), refs.end()); - return tensor->get_leg_idcs(keys); + return tensor->labels.indices(keys); } [[nodiscard]] std::vector @@ -140,12 +140,12 @@ make_tensor_product(std::vector factors, std::move(factors), std::move(symmetry), std::move(sectors), std::move(mults)); } -[[nodiscard]] OptionalLabels -flat_labels(OptionalLabels const& codomain_labels, OptionalLabels const& domain_labels) +[[nodiscard]] LegLabelList +flat_labels(LegLabelList const& codomain_labels, LegLabelList const& domain_labels) { // Flat leg labels are ``codomain_labels + reversed(domain_labels)``, matching // ``parse_tensor_init_labels`` / ``nested_flat_labels`` and Python SymmetricTensor init. - OptionalLabels out = codomain_labels; + LegLabelList out = codomain_labels; out.insert(out.end(), domain_labels.rbegin(), domain_labels.rend()); return out; } @@ -211,16 +211,16 @@ normalize_pipe_dualities(std::optional const& pipe_dualities, std } [[nodiscard]] std::string -format_optional_labels(OptionalLabels const& labs) +format_optional_labels(LegLabelList const& labs) { std::string out = "["; for (std::size_t i = 0; i < labs.size(); ++i) { if (i > 0) { out += ", "; } - if (labs[i]) { + if (!labs[i].empty()) { out += '\''; - out += *labs[i]; + out += labs[i]; out += '\''; } else { out += "None"; @@ -286,18 +286,16 @@ check_same_legs(TensorCPtr t1, TensorCPtr t2) throw std::invalid_argument("Incompatible symmetries"); } bool incompatible_labels = false; - OptionalLabels const labels1 = t1->labels(); - auto const& labelmap2 = t2->labelmap(); + LegLabelList const labels1 = t1->labels.as_list(); for (std::size_t n = 0; n < labels1.size(); ++n) { - if (!labels1[n]) { + if (labels1[n].empty()) { // either l1 is None or l1 not in l2.labels continue; } - auto it = labelmap2.find(*labels1[n]); - if (it == labelmap2.end()) { + if (!t2->labels.has(labels1[n])) { continue; } - if (it->second != static_cast(n)) { + if (t2->labels.index(labels1[n]) != static_cast(n)) { incompatible_labels = true; break; } @@ -311,8 +309,8 @@ check_same_legs(TensorCPtr t1, TensorCPtr t2) std::string msg = "Incompatible legs. "; if (incompatible_labels) { msg += std::format("Should you permute_legs first? t1.labels={} t2.labels={}", - format_optional_labels(t1->labels()), - format_optional_labels(t2->labels())); + format_optional_labels(t1->labels.as_list()), + format_optional_labels(t2->labels.as_list())); } throw std::invalid_argument(msg); } @@ -379,7 +377,7 @@ permute_legs(TensorCPtr tensor, } if (!missing.empty()) { if (as_Hidden(tensor)) { - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); std::vector public_missing; for (auto m : missing) { if (HiddenLegTensor::is_hidden_leg_label(labs[static_cast(m)])) { @@ -526,7 +524,7 @@ permute_legs(TensorCPtr tensor, } if (as_Hidden(tensor)) { - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); int64 min_public = 0; bool any_public_level = false; for (int64 i = 0; i < num_legs; ++i) { @@ -584,9 +582,9 @@ permute_legs(TensorCPtr tensor, levels_v, bend_right_v); - OptionalLabels all_labels = tensor->labels(); - OptionalLabels cod_labels; - OptionalLabels dom_labels; + LegLabelList all_labels = tensor->labels.as_list(); + LegLabelList cod_labels; + LegLabelList dom_labels; for (auto n : codomain_v) { cod_labels.push_back(all_labels[static_cast(n)]); } @@ -711,7 +709,7 @@ combine_legs(TensorCPtr tensor, } if (as_Hidden(tensor)) { - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); for (auto const& group : which_legs_v) { for (auto idx : group) { if (HiddenLegTensor::is_hidden_leg_label(labs[static_cast(idx)])) { @@ -806,12 +804,12 @@ combine_legs(TensorCPtr tensor, auto backend = tensor->backend; std::vector codomain_spaces; - std::vector codomain_labels; - std::vector domain_labels_reversed; + std::vector codomain_labels; + std::vector domain_labels_reversed; std::vector domain_spaces_reversed; std::size_t i = 0; int64 label_offset = 0; - OptionalLabels all_labels = tensor->labels(); + LegLabelList all_labels = tensor->labels.as_list(); for (int64 n = 0; n < N; ++n) { if (codomain_groups.contains(n)) { @@ -825,13 +823,13 @@ combine_legs(TensorCPtr tensor, backend->make_pipe(std::move(spaces_to_combine), pipe_dualities_v[i], pipe_arg); pipes_v[i] = combined; codomain_spaces.push_back(combined); - OptionalLabels group_labels(all_labels.begin() + group.front(), - all_labels.begin() + group.back() + 1); + LegLabelList group_labels(all_labels.begin() + group.front(), + all_labels.begin() + group.back() + 1); codomain_labels.push_back(_combine_leg_labels(group_labels, label_offset)); ++i; int64 none_count = 0; for (auto l : group) { - if (!all_labels[static_cast(l)].has_value()) { + if (all_labels[static_cast(l)].empty()) { ++none_count; } } @@ -849,13 +847,13 @@ combine_legs(TensorCPtr tensor, backend->make_pipe(std::move(spaces_to_combine), !pipe_dualities_v[i], pipe_arg); pipes_v[i] = combined; domain_spaces_reversed.push_back(combined); - OptionalLabels group_labels(all_labels.begin() + group.front(), - all_labels.begin() + group.back() + 1); + LegLabelList group_labels(all_labels.begin() + group.front(), + all_labels.begin() + group.back() + 1); domain_labels_reversed.push_back(_combine_leg_labels(group_labels, label_offset)); ++i; int64 none_count = 0; for (auto l : group) { - if (!all_labels[static_cast(l)].has_value()) { + if (all_labels[static_cast(l)].empty()) { ++none_count; } } @@ -887,7 +885,7 @@ combine_legs(TensorCPtr tensor, } auto data = backend->combine_legs(tensor, which_legs_v, pipes_ptr, codomain_tp, domain_tp); - OptionalLabels res_labels = codomain_labels; + LegLabelList res_labels = codomain_labels; res_labels.insert( res_labels.end(), domain_labels_reversed.begin(), domain_labels_reversed.end()); auto res = make_symmetric_native( @@ -946,7 +944,7 @@ split_legs(TensorCPtr tensor, std::optional> legs) int64 num_legs = tensor->num_legs; if (!legs.has_value()) { - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); for (int64 n = 0; n < tensor->codomain->num_factors; ++n) { auto l = (*tensor->codomain)[n]; if (std::dynamic_pointer_cast(l)) { @@ -972,7 +970,7 @@ split_legs(TensorCPtr tensor, std::optional> legs) } } else { if (as_Hidden(tensor)) { - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); auto sorted = resolve_leg_refs(tensor, *legs); for (auto idx : sorted) { if (HiddenLegTensor::is_hidden_leg_label(labs[static_cast(idx)])) { @@ -1032,8 +1030,8 @@ split_legs(TensorCPtr tensor, std::optional> legs) tensor->domain->sector_decomposition, tensor->domain->multiplicities); - OptionalLabels all_labels = tensor->labels(); - OptionalLabels labels; + LegLabelList all_labels = tensor->labels.as_list(); + LegLabelList labels; std::unordered_set leg_idcs_set(leg_idcs.begin(), leg_idcs.end()); for (int64 idx = 0; idx < static_cast(all_labels.size()); ++idx) { if (leg_idcs_set.contains(idx)) { @@ -1128,8 +1126,8 @@ squeeze_legs(TensorCPtr tensor, std::optional> legs) tensor->domain->sector_decomposition, tensor->domain->multiplicities); - OptionalLabels all_labels = tensor->labels(); - OptionalLabels labels; + LegLabelList all_labels = tensor->labels.as_list(); + LegLabelList labels; for (auto n : remaining) { labels.push_back(all_labels[static_cast(n)]); } @@ -1143,18 +1141,18 @@ namespace { [[nodiscard]] int64 leg_idx(TensorCPtr const& tensor, LegRef const& which) { - return std::visit([&](auto const& key) { return tensor->get_leg_idcs(key).at(0); }, which); + return std::visit([&](auto const& key) { return tensor->labels.index(key); }, which); } [[nodiscard]] std::string unique_temp_label(TensorCPtr const& tensor, std::string base) { - if (!tensor->has_label(base)) { + if (!tensor->labels.has(base)) { return base; } for (int n = 1;; ++n) { auto cand = base + std::to_string(n); - if (!tensor->has_label(cand)) { + if (!tensor->labels.has(cand)) { return cand; } } @@ -1163,7 +1161,7 @@ unique_temp_label(TensorCPtr const& tensor, std::string base) [[nodiscard]] LevelsSpec levels_with_hidden(TensorCPtr const& tensor, int64 hidden_idx) { - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); LevelsSpec levels(static_cast(tensor->num_legs), std::optional{ 0 }); for (int64 i = 0; i < tensor->num_legs; ++i) { if (HiddenLegTensor::is_hidden_leg_label(labs.at(static_cast(i)))) { @@ -1390,7 +1388,7 @@ flatten_pipe_unitary(LegPipe::Ptr const& pipe, Leg::Ptr const& flat, SymmetricTe std::move(pipe_prod), like.backend, symmetry, - OptionalLabels{ "?flat", "?pipe" }); + LegLabelList{ "?flat", "?pipe" }); } /// `flatten_pipe_leg` for the `FusionTreeBackend` by contracting the pipe at ``legs_idx`` with @@ -1406,13 +1404,13 @@ flatten_pipe_leg_by_unitary(SymmetricTensorCPtr const& tensor, { // act on the unhidden tensor, such that the leg indices include hidden legs TensorPtr plain = std::const_pointer_cast(tensor)->as_SymmetricTensor(); - auto const label = plain->labels().at(static_cast(legs_idx)); + auto const label = plain->labels.as_list().at(static_cast(legs_idx)); auto U = flatten_pipe_unitary(pipe, flat, *tensor); // [flat] <- [pipe] if (in_domain) { U = dagger(U); // [pipe] <- [flat] } // the label of the contracted leg of U replaces the pipe label - U->set_labels(in_domain ? OptionalLabels{ "?pipe", label } : OptionalLabels{ label, "?pipe" }); + U->labels.set(in_domain ? LegLabelList{ "?pipe", label } : LegLabelList{ label, "?pipe" }); TensorPtr res; if (product.num_factors == 1) { // partial_compose requires a remaining leg; contract the full (co)domain instead @@ -1427,7 +1425,7 @@ flatten_pipe_leg_by_unitary(SymmetricTensorCPtr const& tensor, res_sym->domain, res_sym->backend, res_sym->symmetry, - tensor->labels(), + tensor->labels.as_list(), false)); } @@ -1484,9 +1482,9 @@ flatten_pipe_leg_at(TensorCPtr tensor, int64 legs_idx) std::move(new_dom), tensor->backend, tensor->symmetry, - tensor->labels(), + tensor->labels.as_list(), /*check_complex_dtype=*/false); - if (HiddenLegTensor::has_hidden_leg_labels(out->labels())) { + if (HiddenLegTensor::has_hidden_leg_labels(out->labels.as_list())) { return std::make_shared(std::move(out)); } return out; @@ -1516,9 +1514,9 @@ replace_legs_space(TensorPtr tensor, int64 legs_idx, Leg::Ptr new_legs_space) std::move(new_dom), tensor->backend, tensor->symmetry, - tensor->labels(), + tensor->labels.as_list(), /*check_complex_dtype=*/false); - if (HiddenLegTensor::has_hidden_leg_labels(out->labels())) { + if (HiddenLegTensor::has_hidden_leg_labels(out->labels.as_list())) { return std::make_shared(std::move(out)); } return out; @@ -1529,18 +1527,18 @@ stripped_hidden_labels_except(HiddenLegTensorCPtr const& tensor, std::optional const& skip_hidden) { std::vector out; - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); for (auto idx : tensor->hidden_leg_idcs()) { auto const& lab = labs.at(static_cast(idx)); - if (!lab) { + if (lab.empty()) { continue; } - if (skip_hidden && *lab == *skip_hidden) { + if (skip_hidden.has_value() && lab == *skip_hidden) { continue; } auto stripped = HiddenLegTensor::strip_hidden_prefix(lab); - if (stripped) { - out.push_back(*stripped); + if (!stripped.empty()) { + out.push_back(stripped); } } return out; @@ -1598,7 +1596,7 @@ move_hidden_leg(HiddenLegTensorCPtr A, throw std::invalid_argument( "move_hidden_leg: specify exactly one of target_codomain_pos and target_domain_pos."); } - if (!HiddenLegTensor::is_hidden_leg_label(OptionalLabel{ hidden_leg_label })) { + if (!HiddenLegTensor::is_hidden_leg_label(hidden_leg_label)) { throw std::invalid_argument(std::format( "move_hidden_leg: hidden_leg_label '{}' must be a hidden label (including '!').", hidden_leg_label)); @@ -1606,13 +1604,13 @@ move_hidden_leg(HiddenLegTensorCPtr A, int64 hidden_idx = -1; try { - hidden_idx = A->get_leg_idcs(hidden_leg_label).at(0); + hidden_idx = A->labels.index(hidden_leg_label); } catch (std::invalid_argument const&) { throw std::invalid_argument( std::format("move_hidden_leg: A has no hidden leg '{}'. Labels are not matching.", hidden_leg_label)); } - auto A_labs = A->labels(); + auto A_labs = A->labels.as_list(); if (!HiddenLegTensor::is_hidden_leg_label(A_labs.at(static_cast(hidden_idx)))) { throw std::invalid_argument( std::format("move_hidden_leg: '{}' is not a hidden leg of A.", hidden_leg_label)); @@ -1623,7 +1621,7 @@ move_hidden_leg(HiddenLegTensorCPtr A, if (HiddenLegTensor::is_hidden_leg_label(A_labs.at(static_cast(axis_A_idx)))) { throw std::invalid_argument("move_hidden_leg: axis_A must be a public leg."); } - auto B_labs = B->labels(); + auto B_labs = B->labels.as_list(); if (HiddenLegTensor::is_hidden_leg_label(B_labs.at(static_cast(axis_B_idx)))) { throw std::invalid_argument("move_hidden_leg: axis_B must be a public leg."); } @@ -1635,21 +1633,21 @@ move_hidden_leg(HiddenLegTensorCPtr A, "move_hidden_leg: axis_A and axis_B are not contractible (must be duals)."); } - if (B->has_label(hidden_leg_label)) { + if (B->labels.has(hidden_leg_label)) { throw std::invalid_argument(std::format( "move_hidden_leg: B already has label '{}'. Relabel one of the hidden legs first.", hidden_leg_label)); } - auto dual_hidden = _dual_leg_label(OptionalLabel{ hidden_leg_label }); - if (dual_hidden && B->has_label(*dual_hidden)) { + auto dual_hidden = _dual_leg_label(hidden_leg_label); + if (!dual_hidden.empty() && B->labels.has(dual_hidden)) { throw std::invalid_argument(std::format( "move_hidden_leg: B already has dual hidden label '{}'. Dual hidden pairs on one " "tensor are not allowed.", - *dual_hidden)); + dual_hidden)); } - auto stripped_h = HiddenLegTensor::strip_hidden_prefix(OptionalLabel{ hidden_leg_label }); - if (!stripped_h) { + auto stripped_h = HiddenLegTensor::strip_hidden_prefix(hidden_leg_label); + if (stripped_h.empty()) { throw std::invalid_argument("move_hidden_leg: hidden_leg_label has no name after '!'."); } auto A_other_hidden = stripped_hidden_labels_except(A, hidden_leg_label); @@ -1675,40 +1673,39 @@ move_hidden_leg(HiddenLegTensorCPtr A, } B_work = std::dynamic_pointer_cast(rehide_labels(B_work, B_other_hidden)); - if (!original_axis_A_label) { + if (original_axis_A_label.empty()) { original_axis_A_label = unique_temp_label(A_work, "_mhl_axisA"); - A_work->set_label(axis_A_idx, original_axis_A_label); + A_work->labels[axis_A_idx] = original_axis_A_label; } - if (!original_axis_B_label) { + if (original_axis_B_label.empty()) { original_axis_B_label = unique_temp_label(B_work, "_mhl_axisB"); - B_work->set_label(axis_B_idx, original_axis_B_label); + B_work->labels[axis_B_idx] = original_axis_B_label; } auto h_leg = A->get_leg(hidden_idx); bool const flatten_1d = h_leg->dim == 1.; A_work = std::dynamic_pointer_cast(move_onto_target_side( - A_work, LegRef{ *stripped_h }, LegRef{ *original_axis_A_label }, /*after=*/false)); + A_work, LegRef{ stripped_h }, LegRef{ original_axis_A_label }, /*after=*/false)); if (!A_work) { throw std::runtime_error( "move_hidden_leg: expected SymmetricTensor after moving A's hidden"); } - auto h_idx_A = A_work->get_leg_idcs(*stripped_h).at(0); + auto h_idx_A = A_work->labels.index(stripped_h); A_work = std::dynamic_pointer_cast( combine_legs(A_work, std::vector>{ - { LegRef{ *stripped_h }, LegRef{ *original_axis_A_label } } }, + { LegRef{ stripped_h }, LegRef{ original_axis_A_label } } }, std::nullopt, std::nullopt, levels_with_hidden(A_work, h_idx_A))); if (!A_work) { throw std::runtime_error("move_hidden_leg: expected SymmetricTensor after combining A"); } - auto pipe_A_label = - _combine_leg_labels({ OptionalLabel{ *stripped_h }, original_axis_A_label }); - int64 pipe_A_idx = A_work->get_leg_idcs(pipe_A_label).at(0); - A_work->set_label(pipe_A_idx, original_axis_A_label); - auto pipe_A = A_work->get_leg(A_work->get_leg_idcs(*original_axis_A_label).at(0)); + auto pipe_A_label = _combine_leg_labels({ stripped_h, original_axis_A_label }); + int64 pipe_A_idx = A_work->labels.index(pipe_A_label); + A_work->labels[pipe_A_idx] = original_axis_A_label; + auto pipe_A = A_work->get_leg(A_work->labels.index(original_axis_A_label)); auto desired_B_legs = pipe_A->dual_leg(); auto h_space = as_space(h_leg); @@ -1719,14 +1716,14 @@ move_hidden_leg(HiddenLegTensorCPtr A, } auto I = eye(h_space, A->backend, - OptionalLabels{ OptionalLabel{ open_lab }, OptionalLabel{ int_lab } }, + LegLabelList{ open_lab, int_lab }, A->dtype, A->device, /*diagonal=*/false); auto B_ext = outer(I, B_work); B_ext = move_onto_target_side( - std::move(B_ext), LegRef{ int_lab }, LegRef{ *original_axis_B_label }, /*after=*/true); - auto int_idx = B_ext->get_leg_idcs(int_lab).at(0); + std::move(B_ext), LegRef{ int_lab }, LegRef{ original_axis_B_label }, /*after=*/true); + auto int_idx = B_ext->labels.index(int_lab); auto [b_in_dom, b_co, b_legs] = B_ext->_parse_leg_idx(int_lab); (void)b_co; (void)b_legs; @@ -1734,36 +1731,36 @@ move_hidden_leg(HiddenLegTensorCPtr A, std::vector b_pipes{ b_in_dom ? pipe_A : desired_B_legs }; B_ext = combine_legs( B_ext, - std::vector>{ { LegRef{ *original_axis_B_label }, LegRef{ int_lab } } }, + std::vector>{ { LegRef{ original_axis_B_label }, LegRef{ int_lab } } }, PipeDualities{ desired_B_legs->is_dual }, b_pipes, levels_with_hidden(B_ext, int_idx)); - auto pipe_B_label = _combine_leg_labels({ original_axis_B_label, OptionalLabel{ int_lab } }); - int64 pipe_B_idx = B_ext->get_leg_idcs(pipe_B_label).at(0); - B_ext->set_label(pipe_B_idx, original_axis_B_label); + auto pipe_B_label = _combine_leg_labels({ original_axis_B_label, int_lab }); + int64 pipe_B_idx = B_ext->labels.index(pipe_B_label); + B_ext->labels[pipe_B_idx] = original_axis_B_label; if (flatten_1d) { A_work = std::dynamic_pointer_cast( - flatten_pipe_leg_at(A_work, A_work->get_leg_idcs(*original_axis_A_label).at(0))); + flatten_pipe_leg_at(A_work, A_work->labels.index(original_axis_A_label))); if (!A_work) { throw std::runtime_error( "move_hidden_leg: expected SymmetricTensor after flattening A"); } - auto a_flat = A_work->get_leg(A_work->get_leg_idcs(*original_axis_A_label).at(0)); + auto a_flat = A_work->get_leg(A_work->labels.index(original_axis_A_label)); B_ext = replace_legs_space( - B_ext, B_ext->get_leg_idcs(*original_axis_B_label).at(0), a_flat->dual_leg()); + B_ext, B_ext->labels.index(original_axis_B_label), a_flat->dual_leg()); } B_ext = move_leg(B_ext, LegRef{ open_lab }, target_codomain_pos, target_domain_pos, - levels_with_hidden(B_ext, B_ext->get_leg_idcs(open_lab).at(0)), + levels_with_hidden(B_ext, B_ext->labels.index(open_lab)), BendRight{ true }); - auto open_idx = B_ext->get_leg_idcs(open_lab).at(0); - B_ext->set_label(open_idx, *stripped_h); + auto open_idx = B_ext->labels.index(open_lab); + B_ext->labels[open_idx] = stripped_h; auto A_out = rehide_labels(A_work, A_other_hidden); std::vector B_hide = B_other_hidden; - B_hide.insert(B_hide.begin(), *stripped_h); + B_hide.insert(B_hide.begin(), stripped_h); auto B_out_tensor = rehide_labels(std::move(B_ext), B_hide); auto B_out = std::dynamic_pointer_cast(B_out_tensor); if (!B_out) { @@ -1858,7 +1855,7 @@ slice_leg(TensorCPtr tensor, LegRef leg, Sector const& sector, int64 multiplicit leg, /*codomain_pos=*/std::nullopt, /*domain_pos=*/0); - moved->set_label(-1, std::string("slice")); + moved->labels[-1] = std::string("slice"); auto inv = std::dynamic_pointer_cast(moved); if (!inv) { inv = moved->as_SymmetricTensor(); diff --git a/src/tensors/planar.cpp b/src/tensors/planar.cpp index a21ebcef..242016a0 100644 --- a/src/tensors/planar.cpp +++ b/src/tensors/planar.cpp @@ -46,10 +46,14 @@ _assert_valid_planar_placeholder_labels(std::vector const& labels, } } +// Separator for globally unique planar labels ("tensor#leg"). +// Must not be in FORBIDDEN_LEG_LABEL_CHARS (unlike diagram LEG_SELECT_SYMBOL ':'). +inline constexpr char PLANAR_UNIQUE_LABEL_SEP = '#'; + [[nodiscard]] bool -_is_hidden_planar_label(OptionalLabel const& label) +_is_hidden_planar_label(std::string const& label) { - if (!label) { + if (label.empty()) { return false; } // HiddenLegTensor hidden legs and ChargedTensor charge-leg markers / temps. @@ -57,29 +61,28 @@ _is_hidden_planar_label(OptionalLabel const& label) HiddenLegTensor::is_charge_temp_label(label)) { return true; } - auto const& s = *label; - auto pos = s.rfind(':'); + auto pos = label.rfind(PLANAR_UNIQUE_LABEL_SEP); if (pos == std::string::npos) { return false; } - OptionalLabel tail{ s.substr(pos + 1) }; + std::string tail{ label.substr(pos + 1) }; return HiddenLegTensor::is_hidden_leg_label(tail) || HiddenLegTensor::is_charge_temp_label(tail); } -[[nodiscard]] bool -_is_hidden_planar_label(std::string const& label) +[[nodiscard]] std::string +planar_unique_label(std::string const& tensor_name, std::string const& leg) { - return _is_hidden_planar_label(OptionalLabel{ label }); + return std::format("{}{}{}", tensor_name, PLANAR_UNIQUE_LABEL_SEP, leg); } [[nodiscard]] std::vector -as_strings(OptionalLabels const& labels) +as_strings(LegLabelList const& labels) { std::vector out; out.reserve(labels.size()); for (auto const& l : labels) { - out.push_back(l.value_or("None")); + out.push_back(l.empty() ? "None" : l); } return out; } @@ -88,7 +91,7 @@ as_strings(OptionalLabels const& labels) _public_string_labels(TensorCPtr tensor) { std::vector out; - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); // ChargedTensor exposes public labels only (no charge leg). if (std::dynamic_pointer_cast(tensor)) { return as_strings(labs); @@ -97,7 +100,7 @@ _public_string_labels(TensorCPtr tensor) if (HiddenLegTensor::is_hidden_leg_label(l) || HiddenLegTensor::is_charge_temp_label(l)) { continue; } - out.push_back(l.value_or("None")); + out.push_back(l.empty() ? "None" : l); } return out; } @@ -263,10 +266,10 @@ strip_ws(std::string s) return s.substr(a, b - a + 1); } -[[nodiscard]] OptionalLabels +[[nodiscard]] LegLabelList labels_from_strings(std::vector const& labels) { - OptionalLabels out; + LegLabelList out; out.reserve(labels.size()); for (auto const& l : labels) { out.emplace_back(l); @@ -326,7 +329,7 @@ planar_decomposition(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, PlanarDecompWhich which, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, std::optional sort = std::nullopt, std::optional algorithm = std::nullopt, @@ -374,16 +377,15 @@ planar_decomposition(TensorCPtr tensor, B = std::move(q); } else if (which == PlanarDecompWhich::eigh) { // eigh returns W, V, where V is the unitary -> permute legs as, e.g., Q in QR - auto [W, V] = - eigh(to_decompose, new_labels.value_or(OptionalLabels{}), new_leg_dual, sort); + auto [W, V] = eigh(to_decompose, new_labels.value_or(LegLabelList{}), new_leg_dual, sort); B = std::move(W); A = std::move(V); } else if (which == PlanarDecompWhich::eig) { - auto [W, V] = eig(to_decompose, new_labels.value_or(OptionalLabels{}), new_leg_dual, sort); + auto [W, V] = eig(to_decompose, new_labels.value_or(LegLabelList{}), new_leg_dual, sort); B = std::move(W); A = std::move(V); } else if (which == PlanarDecompWhich::eigvals) { - auto W = eigvals(to_decompose, new_labels.value_or(OptionalLabels{}), new_leg_dual, sort); + auto W = eigvals(to_decompose, new_labels.value_or(LegLabelList{}), new_leg_dual, sort); B = std::move(W); } else if (which == PlanarDecompWhich::svd) { auto [u, s, vh] = svd(to_decompose, new_labels, new_leg_dual, true, algorithm); @@ -443,7 +445,7 @@ std::string _as_valid_name(std::string name) { name = strip_ws(std::move(name)); - if (!is_valid_leg_label(OptionalLabel{ name })) { + if (!is_valid_leg_label(name)) { throw std::invalid_argument(std::format("Invalid name or leg label: {}", name)); } return name; @@ -528,7 +530,8 @@ _split_tensor_text(std::string const& text) TensorPlaceholder::TensorPlaceholder(std::vector labels, std::vector dims, BigOPolynomial cost_to_make) - : LabelledLegs(labels_from_strings(labels)) + : labels(labels_from_strings(labels)) + , num_legs(static_cast(labels.size())) , cost_to_make(std::move(cost_to_make)) { if (dims.empty()) { @@ -551,7 +554,7 @@ TensorPlaceholder::copy(bool /*deep*/) const std::vector TensorPlaceholder::string_labels() const { - return as_strings(labels()); + return as_strings(labels.as_list()); } std::string @@ -998,39 +1001,39 @@ PlanarDiagram::evaluate(std::map tensors) const std::map working; for (auto const& [name, t] : prepared) { std::map mapping; - for (auto const& l : t->labels()) { - if (!l || _is_hidden_planar_label(l)) { + for (auto const& l : t->labels.as_list()) { + if (l.empty() || _is_hidden_planar_label(l)) { continue; } - mapping[*l] = std::format("{}:{}", name, *l); + mapping[l] = planar_unique_label(name, l); } - t->relabel(mapping); + t->labels.update(mapping); working.emplace(name, t); } std::vector> traces; std::vector> contractions; std::vector> open_legs; for (auto const& [t1, l1, t2, l2] : definition) { - auto rel_l1 = std::format("{}:{}", t1, l1); + auto rel_l1 = planar_unique_label(t1, l1); auto t1_tens = require_tensor(working.at(t1)); - bool t1_has = t1_tens->has_label(rel_l1); + bool t1_has = t1_tens->labels.has(rel_l1); if (!t2) { if (!t1_has) { throw std::invalid_argument(std::format("Missing open leg {}:{}", t1, l1)); } open_legs.emplace_back(rel_l1, l2); } else if (t1 == *t2) { - auto rel_l2 = std::format("{}:{}", t1, l2); - bool t2_has = t1_tens->has_label(rel_l2); + auto rel_l2 = planar_unique_label(t1, l2); + bool t2_has = t1_tens->labels.has(rel_l2); if (!t1_has || !t2_has) { throw std::invalid_argument( std::format("Missing trace legs {}:{}, {}:{}", t1, l1, t1, l2)); } traces.emplace_back(t1, rel_l1, rel_l2); } else { - auto rel_l2 = std::format("{}:{}", *t2, l2); + auto rel_l2 = planar_unique_label(*t2, l2); auto t2_tens = require_tensor(working.at(*t2)); - bool t2_has = t2_tens->has_label(rel_l2); + bool t2_has = t2_tens->labels.has(rel_l2); if (!t1_has || !t2_has) { throw std::invalid_argument( std::format("Missing contraction legs {}:{}, {}:{}", t1, l1, *t2, l2)); @@ -1067,32 +1070,32 @@ PlanarDiagram::evaluate(std::map tensors) const for (auto& [name, t] : prepared) { std::map mapping; for (auto const& l : t.string_labels()) { - mapping[l] = std::format("{}:{}", name, l); + mapping[l] = planar_unique_label(name, l); } - t.relabel(mapping); + t.labels.update(mapping); } std::vector> traces; std::vector> contractions; std::vector> open_legs; for (auto const& [t1, l1, t2, l2] : definition) { - auto rel_l1 = std::format("{}:{}", t1, l1); - bool t1_has = prepared.at(t1).has_label(rel_l1); + auto rel_l1 = planar_unique_label(t1, l1); + bool t1_has = prepared.at(t1).labels.has(rel_l1); if (!t2) { if (!t1_has) { throw std::invalid_argument(std::format("Missing open leg {}:{}", t1, l1)); } open_legs.emplace_back(rel_l1, l2); } else if (t1 == *t2) { - auto rel_l2 = std::format("{}:{}", t1, l2); - bool t2_has = prepared.at(t1).has_label(rel_l2); + auto rel_l2 = planar_unique_label(t1, l2); + bool t2_has = prepared.at(t1).labels.has(rel_l2); if (!t1_has || !t2_has) { throw std::invalid_argument( std::format("Missing trace legs {}:{}, {}:{}", t1, l1, t1, l2)); } traces.emplace_back(t1, rel_l1, rel_l2); } else { - auto rel_l2 = std::format("{}:{}", *t2, l2); - bool t2_has = prepared.at(*t2).has_label(rel_l2); + auto rel_l2 = planar_unique_label(*t2, l2); + bool t2_has = prepared.at(*t2).labels.has(rel_l2); if (!t1_has || !t2_has) { throw std::invalid_argument( std::format("Missing contraction legs {}:{}, {}:{}", t1, l1, *t2, l2)); @@ -1374,14 +1377,13 @@ PlanarDiagram::verify_diagram() int64 num_legs = 0; for (auto const& [t1, l1, t2, l2] : definition) { check(tensors.contains(t1), std::format("No tensor with name {}", t1)); - check(tensors.at(t1).has_label(l1), std::format("Tensor {} has no leg {}", t1, l1)); + check(tensors.at(t1).labels.has(l1), std::format("Tensor {} has no leg {}", t1, l1)); num_legs += 1; if (!t2) { - check(is_valid_leg_label(OptionalLabel{ l2 }), - std::format("Invalid leg label {}", l2)); + check(is_valid_leg_label(l2), std::format("Invalid leg label {}", l2)); } else { check(tensors.contains(*t2), std::format("No tensor with name {}", *t2)); - check(tensors.at(*t2).has_label(l2), std::format("Tensor {} has no leg {}", *t2, l2)); + check(tensors.at(*t2).labels.has(l2), std::format("Tensor {} has no leg {}", *t2, l2)); num_legs += 1; } } @@ -1405,9 +1407,9 @@ PlanarDiagram::verify_diagram() // - figures out the cost auto res = evaluate(tensors); std::vector ol; - for (auto const& l : res.labels()) { + for (auto const& l : res.labels.as_list()) { if (!_is_hidden_planar_label(l)) { - ol.push_back(l.value_or("None")); + ol.push_back(l.empty() ? "None" : l); } } return { std::move(ol), res.cost_to_make }; @@ -1567,9 +1569,9 @@ PlanarDiagram::_extract_result(std::map const& tensor } auto t = std::get(tens); std::vector visible_labels; - for (auto const& l : t->labels()) { + for (auto const& l : t->labels.as_list()) { if (!_is_hidden_planar_label(l)) { - visible_labels.push_back(l.value_or("None")); + visible_labels.push_back(l.empty() ? "None" : l); } } if (visible_open_legs.empty()) { @@ -1595,7 +1597,7 @@ PlanarDiagram::_extract_result(std::map const& tensor for (auto const& [old, neu] : visible_open_legs) { mapping[old] = neu; } - t->relabel(mapping); + t->labels.update(mapping); return t; } @@ -1614,9 +1616,9 @@ PlanarDiagram::_extract_result(std::map const& t } } std::vector visible_labels; - for (auto const& l : tens.labels()) { + for (auto const& l : tens.labels.as_list()) { if (!_is_hidden_planar_label(l)) { - visible_labels.push_back(l.value_or("None")); + visible_labels.push_back(l.empty() ? "None" : l); } } if (visible_open_legs.empty()) { @@ -1642,7 +1644,7 @@ PlanarDiagram::_extract_result(std::map const& t for (auto const& [old, neu] : visible_open_legs) { mapping[old] = neu; } - tens.relabel(mapping); + tens.labels.update(mapping); return tens; } @@ -1757,7 +1759,7 @@ std::tuple horizontal_factorization(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, std::optional cutoff_singular_values) { if (!cutoff_singular_values) { @@ -1783,18 +1785,18 @@ horizontal_factorization(TensorCPtr tensor, bool planar_almost_equal(TensorCPtr tensor_1, TensorCPtr tensor_2, float64 rtol, float64 atol) { - auto labs1 = tensor_1->labels(); - auto labs2 = tensor_2->labels(); - if (contains(labs1, OptionalLabel{}) || contains(labs2, OptionalLabel{})) { + auto labs1 = tensor_1->labels.as_list(); + auto labs2 = tensor_2->labels.as_list(); + if (contains(labs1, "") || contains(labs2, "")) { throw std::invalid_argument("Can only compare tensors for which each leg has a label"); } std::set s1; std::set s2; for (auto const& l : labs1) { - s1.insert(*l); + s1.insert(l); } for (auto const& l : labs2) { - s2.insert(*l); + s2.insert(l); } if (s1 != s2) { throw std::invalid_argument("Both tensors need to have the same leg labels"); @@ -1802,10 +1804,10 @@ planar_almost_equal(TensorCPtr tensor_1, TensorCPtr tensor_2, float64 rtol, floa std::vector codomain; std::vector domain; for (int64 i = 0; i < tensor_2->num_codomain_legs(); ++i) { - codomain.push_back(*labs2[static_cast(i)]); + codomain.push_back(labs2[static_cast(i)]); } for (int64 i = tensor_2->num_legs - 1; i >= tensor_2->num_codomain_legs(); --i) { - domain.push_back(*labs2[static_cast(i)]); + domain.push_back(labs2[static_cast(i)]); } auto t1 = planar_permute_legs(tensor_1, as_leg_refs(codomain), as_leg_refs(domain)); return almost_equal(t1, tensor_2, rtol, atol, true); @@ -1820,7 +1822,7 @@ planar_combine_legs(TensorCPtr T, std::vector> which; which.reserve(which_legs.size()); for (auto const& group : which_legs) { - which.push_back(T->get_leg_idcs(group)); + which.push_back(T->labels.indices(group)); } // identify if is there is a group on the left / right with legs that need // to be bent and where to combine them @@ -1960,27 +1962,27 @@ _hidden_dual_label_pairs(TensorCPtr tensor1, TensorCPtr tensor2) if (!h1 || !h2) { return {}; } - auto labs1 = tensor1->labels(); - auto labs2 = tensor2->labels(); + auto labs1 = tensor1->labels.as_list(); + auto labs2 = tensor2->labels.as_list(); std::vector> hidden1; std::vector> hidden2; for (int64 i = 0; i < tensor1->num_legs; ++i) { auto const& lab = labs1[static_cast(i)]; if (HiddenLegTensor::is_hidden_leg_label(lab)) { - hidden1.emplace_back(i, *lab); + hidden1.emplace_back(i, lab); } } for (int64 i = 0; i < tensor2->num_legs; ++i) { auto const& lab = labs2[static_cast(i)]; if (HiddenLegTensor::is_hidden_leg_label(lab)) { - hidden2.emplace_back(i, *lab); + hidden2.emplace_back(i, lab); } } std::vector> pairs; std::vector used2(hidden2.size(), false); for (auto const& [i1, lab1] : hidden1) { (void)i1; - auto dual1 = _dual_leg_label(OptionalLabel{ lab1 }); + auto dual1 = _dual_leg_label(lab1); for (std::size_t j = 0; j < hidden2.size(); ++j) { if (used2[j]) { continue; @@ -1994,7 +1996,7 @@ _hidden_dual_label_pairs(TensorCPtr tensor1, TensorCPtr tensor2) "implicitly.", lab1)); } - if (dual1 && *dual1 == lab2) { + if (dual1 == lab2) { pairs.emplace_back(lab1, lab2); used2[j] = true; break; @@ -2170,7 +2172,7 @@ _arrange_hidden_around_public_contr(TensorCPtr tensor, // Public cyclic order is unchanged, so public legs do not braid; hidden at // -1 may braid past public at 0. LevelsSpec levels(static_cast(tensor->num_legs), std::optional{ 0 }); - auto labs = tensor->labels(); + auto labs = tensor->labels.as_list(); for (int64 i = 0; i < tensor->num_legs; ++i) { if (HiddenLegTensor::is_hidden_leg_label(labs[static_cast(i)])) { levels[static_cast(i)] = -1; @@ -2211,8 +2213,8 @@ planar_contraction(TensorCPtr tensor1, std::map relabel1, std::map relabel2) { - auto legs1_idcs = tensor1->get_leg_idcs(legs1); - auto legs2_idcs = tensor2->get_leg_idcs(legs2); + auto legs1_idcs = tensor1->labels.indices(legs1); + auto legs2_idcs = tensor2->labels.indices(legs2); auto num_contr = static_cast(legs1_idcs.size()); if (static_cast(legs2_idcs.size()) != num_contr) { throw std::invalid_argument("legs1 and legs2 must have the same length"); @@ -2258,8 +2260,8 @@ planar_contraction(TensorCPtr tensor1, extra1.reserve(dual_labels.size()); extra2.reserve(dual_labels.size()); for (auto const& [l1, l2] : dual_labels) { - extra1.push_back(tensor1->get_leg_idcs(std::vector{ LegRef{ l1 } })[0]); - extra2.push_back(tensor2->get_leg_idcs(std::vector{ LegRef{ l2 } })[0]); + extra1.push_back(tensor1->labels.index(l1)); + extra2.push_back(tensor2->labels.index(l2)); } _check_hidden_circle_planar_contraction(tensor1, tensor2, extra1, extra2); auto [t1_arr, map1] = _arrange_hidden_around_public_contr( @@ -2511,8 +2513,8 @@ planar_contraction(TensorPlaceholder const& tensor1, std::vector legs1, std::vector legs2) { - auto legs1_idcs = tensor1.get_leg_idcs(legs1); - auto legs2_idcs = tensor2.get_leg_idcs(legs2); + auto legs1_idcs = tensor1.labels.indices(legs1); + auto legs2_idcs = tensor2.labels.indices(legs2); auto num_contr = static_cast(legs1_idcs.size()); if (static_cast(legs2_idcs.size()) != num_contr) { throw std::invalid_argument("legs1 and legs2 must have the same length"); @@ -2568,7 +2570,7 @@ std::tuple planar_eigh(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, std::optional sort) { @@ -2590,7 +2592,7 @@ std::tuple planar_eig(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, std::optional sort) { @@ -2612,7 +2614,7 @@ DiagonalTensorPtr planar_eigvals(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, std::optional sort) { @@ -2634,7 +2636,7 @@ std::tuple planar_lq(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual) { auto r = planar_decomposition(tensor, @@ -2652,7 +2654,7 @@ planar_partial_trace(TensorCPtr tensor, std::vector> pairs) std::vector> pair_idcs; pair_idcs.reserve(pairs.size()); for (auto const& p : pairs) { - pair_idcs.push_back(tensor->get_leg_idcs(p)); + pair_idcs.push_back(tensor->labels.indices(p)); } std::vector traced_legs; for (auto const& p : pair_idcs) { @@ -2836,7 +2838,7 @@ planar_partial_trace(TensorPlaceholder const& tensor, std::vector> pair_idcs; pair_idcs.reserve(pairs.size()); for (auto const& p : pairs) { - pair_idcs.push_back(tensor.get_leg_idcs(p)); + pair_idcs.push_back(tensor.labels.indices(p)); } std::vector traced_legs; for (auto const& p : pair_idcs) { @@ -3398,10 +3400,10 @@ planar_permute_legs(TensorCPtr T, std::optional> domain_idcs; std::optional> codomain_idcs; if (domain) { - domain_idcs = T->get_leg_idcs(*domain); + domain_idcs = T->labels.indices(*domain); } if (codomain) { - codomain_idcs = T->get_leg_idcs(*codomain); + codomain_idcs = T->labels.indices(*codomain); } // Hidden legs live on a second plane: if only public legs are specified, the permutation @@ -3439,7 +3441,7 @@ std::tuple planar_qr(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual) { auto r = planar_decomposition(tensor, @@ -3455,7 +3457,7 @@ std::tuple planar_svd(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, std::optional algorithm) { @@ -3474,7 +3476,7 @@ std::tuple planar_truncated_svd(TensorCPtr tensor, int64 codomain_cut, int64 domain_cut, - std::optional new_labels, + std::optional new_labels, bool new_leg_dual, std::optional algorithm, std::optional normalize_to, diff --git a/src/tensors/sparse.cpp b/src/tensors/sparse.cpp index ee6bbc66..950f662e 100644 --- a/src/tensors/sparse.cpp +++ b/src/tensors/sparse.cpp @@ -77,16 +77,8 @@ TensorLinearOperator::TensorLinearOperator(SymmetricTensorPtr tensor, throw std::invalid_argument("Expected a two-leg tensor"); } - std::vector idcs; - if (std::holds_alternative(which_leg_ref)) { - idcs = this->tensor->get_leg_idcs(std::get(which_leg_ref)); - } else { - idcs = this->tensor->get_leg_idcs(std::get(which_leg_ref)); - } - if (idcs.size() != 1) { - throw std::invalid_argument("which_leg must refer to a single leg"); - } - this->which_leg = idcs[0]; + this->which_leg = + std::visit([&](auto const& key) { return this->tensor->labels.index(key); }, which_leg_ref); this->other_leg = 1 - this->which_leg; auto legs = this->tensor->legs(); @@ -96,8 +88,8 @@ TensorLinearOperator::TensorLinearOperator(SymmetricTensorPtr tensor, } this->vector_legs = { legs[other_leg] }; - auto labels = this->tensor->labels(); - this->vector_labels = VectorLabels{ OptionalLabels{ labels[other_leg] } }; + auto labels = this->tensor->labels.as_list(); + this->vector_labels = VectorLabels{ LegLabelList{ labels[other_leg] } }; this->dtype = this->tensor->dtype; } diff --git a/src/tensors/symmetric_tensor.cpp b/src/tensors/symmetric_tensor.cpp index 4333cfa7..727a229f 100644 --- a/src/tensors/symmetric_tensor.cpp +++ b/src/tensors/symmetric_tensor.cpp @@ -45,7 +45,7 @@ SymmetricTensor::SymmetricTensor(TensorBackend::DataPtr data_in, TensorProduct::Ptr domain_in, TensorBackend::Ptr backend_in, Symmetry::Ptr symmetry_in, - OptionalLabels labels_in, + LegLabelList labels_in, bool check_complex_dtype) : Tensor(std::move(codomain_in), std::move(domain_in), @@ -59,8 +59,8 @@ SymmetricTensor::SymmetricTensor(TensorBackend::DataPtr data_in, assert(backend->is_correct_data_type(data)); // Allow the ChargedTensor charge-leg marker "!" as the last label on bare // SymmetricTensors that are used as invariant parts (tests / factories). - auto const labs = labels(); - if (!labs.empty() && labs.back() && *labs.back() == "!") { + auto const labs = labels.as_list(); + if (!labs.empty() && labs.back() == "!") { allow_charge_leg_label = true; } if (check_complex_dtype) { @@ -89,19 +89,19 @@ SymmetricTensor::test_sanity() const // HiddenLegTensor validates `!` itself; ChargedTensor invariant parts set // allow_charge_leg_label. All other SymmetricTensors must not contain `!`. if (dynamic_cast(this) == nullptr && !allow_charge_leg_label) { - reject_exclamation_in_labels(labels(), "SymmetricTensor"); + reject_exclamation_in_labels(labels.as_list(), "SymmetricTensor"); } else if (allow_charge_leg_label) { - auto labs = labels(); + auto labs = labels.as_list(); for (std::size_t i = 0; i < labs.size(); ++i) { - if (!labs[i] || !label_contains_exclamation(labs[i])) { + if (labs[i].empty() || !label_contains_exclamation(labs[i])) { continue; } // Only the charge leg (last label, starts with '!') is allowed. - if (i + 1 != labs.size() || !labs[i]->starts_with('!')) { + if (i + 1 != labs.size() || !labs[i].starts_with('!')) { throw std::invalid_argument(std::format( "ChargedTensor invariant_part: only the last label may contain '!', got '{}' " "at position {}", - *labs[i], + labs[i], i)); } } @@ -148,7 +148,7 @@ SymmetricTensor::Ptr SymmetricTensor::from_zero(TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -170,7 +170,7 @@ SymmetricTensor::from_zero(TensorProduct::Ptr codomain, SymmetricTensor::Ptr SymmetricTensor::from_eye(TensorProduct::Ptr co_domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -192,7 +192,7 @@ SymmetricTensor::from_block_func(BlockFactoryFn func, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -227,7 +227,7 @@ SymmetricTensor::from_sector_block_func(SectorBlockFactoryFn func, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -254,7 +254,7 @@ SymmetricTensor::from_dense_block(BlockBackend::BlockPtr block, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device, float64 tol, @@ -299,7 +299,7 @@ SymmetricTensor::from_dense_block_trivial_sector(BlockBackend::BlockPtr vector, Leg::Ptr space, TensorBackend::Ptr backend, std::optional device, - OptionalLabel label) + std::string label) { if (!space) { throw std::invalid_argument("space must be specified"); @@ -339,7 +339,7 @@ SymmetricTensor::from_dense_block_trivial_sector(BlockBackend::BlockPtr vector, std::make_shared(std::vector{ space }, space->symmetry); auto domain = std::make_shared(std::vector{}, space->symmetry); return std::make_shared( - data, codomain, domain, backend, space->symmetry, OptionalLabels{ std::move(label) }); + data, codomain, domain, backend, space->symmetry, LegLabelList{ std::move(label) }); } SymmetricTensor::Ptr @@ -348,7 +348,7 @@ SymmetricTensor::from_random_normal(TensorProduct::Ptr codomain, TensorCPtr mean, float64 sigma, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -388,9 +388,9 @@ SymmetricTensor::from_random_normal(TensorProduct::Ptr codomain, backend_tp = std::move(b); symmetry = std::move(s); if (!labels.has_value()) { - labels = mean->labels(); + labels = mean->labels.as_list(); } else { - if (mean->labels() != _init_parse_labels(labels, codomain_tp, domain_tp)) { + if (mean->labels.as_list() != _init_parse_labels(labels, codomain_tp, domain_tp)) { throw std::invalid_argument("mean.labels must match the given labels"); } } @@ -435,8 +435,12 @@ SymmetricTensor::from_random_normal(TensorProduct::Ptr codomain, if (mean) { auto one = backend_tp->block_backend->as_scalar(1.0); auto new_data = backend_tp->linear_combination(one, mean, one, with_zero_mean); - return std::make_shared( - new_data, codomain_tp, domain_tp, backend_tp, symmetry, with_zero_mean->labels()); + return std::make_shared(new_data, + codomain_tp, + domain_tp, + backend_tp, + symmetry, + with_zero_mean->labels.as_list()); } return with_zero_mean; } @@ -445,7 +449,7 @@ SymmetricTensor::Ptr SymmetricTensor::from_random_uniform(TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, Dtype dtype, std::optional device) { @@ -471,7 +475,7 @@ SymmetricTensor::Ptr SymmetricTensor::from_sector_projection(TensorProduct::Ptr co_domain, Sector sector, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -519,7 +523,7 @@ SymmetricTensor::from_tree_pairs(py::object trees_obj, TensorProduct::Ptr codomain, TensorProduct::Ptr domain, TensorBackend::Ptr backend, - std::optional labels, + std::optional labels, std::optional dtype, std::optional device) { @@ -615,7 +619,7 @@ SymmetricTensor::as_dtype(Dtype new_dtype) } auto new_data = backend->to_dtype(shared_from_this(), new_dtype); return std::make_shared( - new_data, codomain, domain, backend, symmetry, labels()); + new_data, codomain, domain, backend, symmetry, labels.as_list()); } SymmetricTensorPtr @@ -647,7 +651,7 @@ SymmetricTensor::copy(bool deep, new_data = data; } return std::make_shared( - new_data, codomain, domain, backend, symmetry, labels()); + new_data, codomain, domain, backend, symmetry, labels.as_list()); } DiagonalTensorPtr @@ -787,7 +791,7 @@ SymmetricTensor::to_backend(TensorBackend::Ptr new_backend, } return std::make_shared( - new_data, codomain, domain, new_backend, symmetry, labels()); + new_data, codomain, domain, new_backend, symmetry, labels.as_list()); } BlockBackend::BlockPtr @@ -814,8 +818,8 @@ SymmetricTensor::to_dense_block( block = backend->block_backend->to_dtype(block, *dtype_opt); } if (leg_order.has_value()) { - auto idcs = get_leg_idcs(*leg_order); - auto const labs = labels(); + auto idcs = labels.indices(*leg_order); + auto const labs = labels.as_list(); std::vector hidden; for (int64 i = 0; i < num_legs; ++i) { if (HiddenLegTensor::is_hidden_leg_label(labs[static_cast(i)])) { @@ -887,7 +891,7 @@ SymmetricTensor::save_hdf5(lasyten::hdf5::Saver& saver, hdf5_io::h5_set_attr(h5gr.getId(), "num_legs", static_cast(num_legs)); hdf5_export::save_f64_vector(saver, subpath + "shape", shape); hdf5_io::h5_set_attr(h5gr.getId(), "cls", class_name()); - hdf5_export::save_optional_labels(saver, subpath + "labels", _labels); + hdf5_export::save_leg_labels(saver, subpath + "labels", labels.as_list()); } SymmetricTensor::Ptr @@ -907,10 +911,10 @@ SymmetricTensor::from_hdf5(lasyten::hdf5::Loader& loader, } auto dt = hdf5_export::load_dtype_string(loader, subpath + "dtype"); auto shape = hdf5_export::load_f64_vector(loader, subpath + "shape"); - auto labels = hdf5_export::load_optional_labels(loader, subpath + "labels"); + auto labels = hdf5_export::load_leg_labels(loader, subpath + "labels"); int64 nlegs = codomain->num_factors + domain->num_factors; if (labels.empty() && nlegs > 0) { - labels.assign(static_cast(nlegs), std::nullopt); + labels.assign(static_cast(nlegs), ""); } auto obj = std::make_shared( diff --git a/src/tensors/tensor.cpp b/src/tensors/tensor.cpp index 82da3472..9e8a11e2 100644 --- a/src/tensors/tensor.cpp +++ b/src/tensors/tensor.cpp @@ -51,19 +51,19 @@ factor_dim(py::handle factor) return factor.attr("dim").cast(); } -OptionalLabel +std::string as_leg_label(py::handle obj) { if (obj.is_none()) { - return std::nullopt; + return ""; } return obj.cast(); } -OptionalLabels +LegLabelList sequence_as_leg_labels(py::handle seq) { - OptionalLabels out; + LegLabelList out; for (auto item : seq) { out.push_back(as_leg_label(item)); } @@ -160,10 +160,11 @@ Tensor::Tensor(TensorProduct::Ptr codomain_, TensorProduct::Ptr domain_, TensorBackend::Ptr backend_, Symmetry::Ptr symmetry_, - OptionalLabels labels, + LegLabelList labels_in, Dtype dtype_, std::string device_) - : LabelledLegs(std::move(labels)) + : labels(std::move(labels_in)) + , num_legs(labels.num_legs()) , codomain(std::move(codomain_)) , domain(std::move(domain_)) , backend(std::move(backend_)) @@ -203,6 +204,16 @@ Tensor::Tensor(TensorProduct::Ptr codomain_, throw std::invalid_argument(std::format( "expected {} labels, got {}", codomain->num_factors + domain->num_factors, num_legs)); } + + // Keep num_legs in sync if labels are replaced (size is fixed for a live tensor). + labels.set_hooks( + [this](LegLabels& /*self*/, LegLabelList const& proposed) { + if (static_cast(proposed.size()) != num_legs) { + throw std::invalid_argument( + std::format("expected {} labels, got {}", num_legs, proposed.size())); + } + }, + [this](LegLabels& self) { num_legs = self.num_legs(); }); } Tensor::Tensor(InitParsed init, Dtype dtype, std::string device) @@ -262,8 +273,8 @@ Tensor::_init_parse_args(TensorProduct::Ptr codomain, return { std::move(codomain), std::move(domain), std::move(backend), std::move(symmetry) }; } -OptionalLabels -Tensor::_init_parse_labels(std::optional labels, +LegLabelList +Tensor::_init_parse_labels(std::optional labels, TensorProduct::Ptr const& codomain, TensorProduct::Ptr const& domain, bool is_endomorphism) @@ -274,7 +285,7 @@ Tensor::_init_parse_labels(std::optional labels, "endomorphism requires the same number of domain and codomain legs"); } if (!labels.has_value()) { - return OptionalLabels(static_cast(num_legs), std::nullopt); + return LegLabelList(static_cast(num_legs), ""); } if (labels->empty()) { if (num_legs != 0) { @@ -283,7 +294,7 @@ Tensor::_init_parse_labels(std::optional labels, return {}; } if (is_endomorphism && static_cast(labels->size()) == codomain->num_factors) { - OptionalLabels out = *labels; + LegLabelList out = *labels; for (auto it = labels->rbegin(); it != labels->rend(); ++it) { out.push_back(_dual_leg_label(*it)); } @@ -339,7 +350,7 @@ parse_tensor_init_args(py::object codomain, py::object domain, TensorBackend::Pt std::move(codomain_tp), std::move(domain_tp), std::move(backend)); } -OptionalLabels +LegLabelList parse_tensor_init_labels(py::object labels, TensorProduct::Ptr const& codomain, TensorProduct::Ptr const& domain, @@ -364,7 +375,7 @@ parse_tensor_init_labels(py::object labels, py::sequence seq = labels.cast(); if (py::len(seq) == 0) { - return Tensor::_init_parse_labels(OptionalLabels{}, codomain, domain, is_endomorphism); + return Tensor::_init_parse_labels(LegLabelList{}, codomain, domain, is_endomorphism); } py::object first = seq[py::int_(0)]; @@ -374,8 +385,8 @@ parse_tensor_init_labels(py::object labels, } py::object codomain_labels_obj = seq[py::int_(0)]; py::object domain_labels_obj = seq[py::int_(1)]; - OptionalLabels codomain_labels; - OptionalLabels domain_labels; + LegLabelList codomain_labels; + LegLabelList domain_labels; if (codomain_labels_obj.is_none()) { if (is_endomorphism && !domain_labels_obj.is_none()) { for (auto item : domain_labels_obj) { @@ -383,7 +394,7 @@ parse_tensor_init_labels(py::object labels, } } else { codomain_labels = - OptionalLabels(static_cast(codomain->num_factors), std::nullopt); + LegLabelList(static_cast(codomain->num_factors), ""); } } else { codomain_labels = sequence_as_leg_labels(codomain_labels_obj); @@ -400,8 +411,7 @@ parse_tensor_init_labels(py::object labels, domain_labels.push_back(_dual_leg_label(l)); } } else { - domain_labels = - OptionalLabels(static_cast(domain->num_factors), std::nullopt); + domain_labels = LegLabelList(static_cast(domain->num_factors), ""); } } else { domain_labels = sequence_as_leg_labels(domain_labels_obj); @@ -411,14 +421,14 @@ parse_tensor_init_labels(py::object labels, "expected {} domain labels, got {}", domain->num_factors, domain_labels.size())); } - OptionalLabels out = codomain_labels; + LegLabelList out = codomain_labels; for (auto it = domain_labels.rbegin(); it != domain_labels.rend(); ++it) { out.push_back(*it); } return out; } - OptionalLabels flat = sequence_as_leg_labels(seq); + LegLabelList flat = sequence_as_leg_labels(seq); return Tensor::_init_parse_labels(std::move(flat), codomain, domain, is_endomorphism); } @@ -450,7 +460,7 @@ Tensor::test_sanity() const for (auto const& leg : codomain->factors) { (void)leg; } - LabelledLegs::test_sanity(); + labels.test_sanity(); } std::string @@ -516,7 +526,7 @@ Tensor::ascii_diagram() const std::vector codomain_labels_s; std::vector domain_labels_s; for (auto const& l : c_labs) { - std::string s = l ? *l : "None"; + std::string s = l.empty() ? "None" : l; if (static_cast(s.size()) <= DISTANCE) { codomain_labels_s.push_back(rjust(s, DISTANCE)); } else { @@ -524,7 +534,7 @@ Tensor::ascii_diagram() const } } for (auto const& l : d_labs) { - std::string s = l ? *l : "None"; + std::string s = l.empty() ? "None" : l; if (static_cast(s.size()) <= DISTANCE) { domain_labels_s.push_back(rjust(s, DISTANCE)); } else { @@ -615,10 +625,11 @@ Tensor::ascii_diagram() const return out.str(); } -OptionalLabels +LegLabelList Tensor::codomain_labels() const { - return OptionalLabels(_labels.begin(), _labels.begin() + num_codomain_legs()); + auto const labs = labels.as_list(); + return LegLabelList(labs.begin(), labs.begin() + num_codomain_legs()); } Tensor::Ptr @@ -627,10 +638,11 @@ Tensor::dagger() const throw NotImplemented("Tensor::dagger (free function dagger not yet converted)"); } -OptionalLabels +LegLabelList Tensor::domain_labels() const { - OptionalLabels out(_labels.begin() + num_codomain_legs(), _labels.end()); + auto const labs = labels.as_list(); + LegLabelList out(labs.begin() + num_codomain_legs(), labs.end()); std::reverse(out.begin(), out.end()); return out; } @@ -750,15 +762,9 @@ Tensor::_parse_leg_idx(std::variant which_leg) const { int64 idx; if (std::holds_alternative(which_leg)) { - auto const& label = std::get(which_leg); - auto it = _labelmap.find(label); - if (it == _labelmap.end()) { - throw std::invalid_argument(std::format( - "No leg with label {}. Labels are {}", label, format_like_list(py::cast(_labels)))); - } - idx = it->second; + idx = labels.index(std::get(which_leg)); } else { - idx = to_valid_idx(std::get(which_leg), num_legs); + idx = labels.index(std::get(which_leg)); } bool const in_domain = idx >= static_cast(codomain->factors.size()); int64 co_domain_idx; @@ -777,12 +783,13 @@ Tensor::_repr_header_lines(std::string const& indent, bool use_symm_str) const // TODO should we put some info still ...? // --- std::string labels_str; - if (std::ranges::all_of(_labels, [](OptionalLabel const& l) { return !l; })) { + auto const labs = labels.as_list(); + if (std::ranges::all_of(labs, [](std::string const& l) { return l.empty(); })) { labels_str = "None"; } else { - // Match Python f'{self._labels} ; {self.codomain_labels} <- {self.domain_labels}' + // Match Python f'{self.labels} ; {self.codomain_labels} <- {self.domain_labels}' labels_str = std::format("{} ; {} <- {}", - format_like_list(py::cast(_labels)), + format_like_list(py::cast(labs)), format_like_list(py::cast(codomain_labels())), format_like_list(py::cast(domain_labels()))); } @@ -884,13 +891,6 @@ Tensor::get_leg_co_domain(std::vector> const& w return out; } -Tensor& -Tensor::set_labels(OptionalLabels labels) -{ - LabelledLegs::set_labels(std::move(labels)); - return *this; -} - py::array Tensor::to_numpy(std::optional>> leg_order, py::object numpy_dtype, diff --git a/src/tools/hdf5_export.cpp b/src/tools/hdf5_export.cpp index 59d43204..a6d3dbc4 100644 --- a/src/tools/hdf5_export.cpp +++ b/src/tools/hdf5_export.cpp @@ -211,9 +211,7 @@ load_optional_i64_vector(lasyten::hdf5::Loader& loader, std::string const& path) } void -save_optional_labels(lasyten::hdf5::Saver& saver, - std::string const& path, - OptionalLabels const& labels) +save_leg_labels(lasyten::hdf5::Saver& saver, std::string const& path, LegLabelList const& labels) { HighFive::Group seq_g; std::string seq_sub; @@ -221,29 +219,26 @@ save_optional_labels(lasyten::hdf5::Saver& saver, path, hdf5_io::REPR_LIST, static_cast(labels.size()), seq_g, seq_sub); lasyten::hdf5::Saver seq_saver(seq_g); for (std::size_t i = 0; i < labels.size(); ++i) { - auto const key = std::to_string(i); - if (labels[i]) { - seq_saver.save_string(key, *labels[i]); - } else { - seq_saver.save_none(key); - } + // Empty string = unset; still stored as a string (not REPR_NONE). + seq_saver.save_string(std::to_string(i), labels[i]); } } -OptionalLabels -load_optional_labels(lasyten::hdf5::Loader& loader, std::string const& path) +LegLabelList +load_leg_labels(lasyten::hdf5::Loader& loader, std::string const& path) { hid_t id = loader.open(path); auto const n = sequence_len(loader, id); HighFive::Group seq_g = hdf5_io::group_from_hid(id); lasyten::hdf5::Loader seq_loader(seq_g); - OptionalLabels out; + LegLabelList out; out.reserve(static_cast(n)); for (std::int64_t i = 0; i < n; ++i) { hid_t child = seq_loader.open(std::to_string(i)); auto type = seq_loader.type_of(child); + // Backward compat: older files stored unset labels as HDF5 None. if (type == hdf5_io::REPR_NONE) { - out.emplace_back(std::nullopt); + out.emplace_back(""); } else { out.emplace_back(seq_loader.load_string(child)); } diff --git a/tests/python_tests/backends/test_fusion_tree_backend.py b/tests/python_tests/backends/test_fusion_tree_backend.py index fc750131..becc890b 100644 --- a/tests/python_tests/backends/test_fusion_tree_backend.py +++ b/tests/python_tests/backends/test_fusion_tree_backend.py @@ -1976,7 +1976,7 @@ def cross_check_single_c_symbol_tree_blocks( # NOTE the case of braiding in domain and codomain are treated separately despite being similar # This way, it is easier to understand the more compact and more efficient function ftb = fusion_tree_backend - index = ten.get_leg_idcs(leg)[0] + index = ten.labels.index(leg) block_backend = ten.backend.block_backend symmetry = ten.symmetry @@ -2150,7 +2150,7 @@ def cross_check_single_c_symbol_tree_cols( NOTE this function may be deleted at a later stage """ ftb = fusion_tree_backend - index = ten.get_leg_idcs(leg)[0] + index = ten.labels.index(leg) block_backend = ten.backend.block_backend symmetry = ten.symmetry diff --git a/tests/python_tests/models/test_couplings.py b/tests/python_tests/models/test_couplings.py index dff47ca4..1d98016d 100644 --- a/tests/python_tests/models/test_couplings.py +++ b/tests/python_tests/models/test_couplings.py @@ -195,7 +195,7 @@ def test_spin_spin_coupling(any_backend, np_random): if site1 == site2: # commutation relation tensor_commuted = tensors.permute_legs(tensor, codomain=[1, 0], domain=[2, 3]) - tensor_commuted.relabel({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) + tensor_commuted.labels.update({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) assert tensors.almost_equal(tensor_commuted, tensor) # check eigenvalues of special cases @@ -277,7 +277,7 @@ def test_aklt_coupling(any_backend, np_random): if site1 == site2: # commutation relation tensor_commuted = tensors.permute_legs(tensor, codomain=[1, 0], domain=[2, 3]) - tensor_commuted.relabel({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) + tensor_commuted.labels.update({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) assert tensors.almost_equal(tensor_commuted, tensor) if isinstance(site1, sites.SpinSite): @@ -323,7 +323,7 @@ def test_chiral_3spin_coupling(any_backend, np_random): # cyclic permutation relation tensor_commuted = tensors.permute_legs(tensor, codomain=[2, 0, 1], domain=[3, 5, 4]) relabel = {'p2': 'p0', 'p1': 'p2', 'p0': 'p1', 'p2*': 'p0*', 'p1*': 'p2*', 'p0*': 'p1*'} - tensor_commuted.relabel(relabel) + tensor_commuted.labels.update(relabel) assert tensors.almost_equal(tensor_commuted, tensor) check_coupling(couplings.chiral_3spin_coupling, site_num=3, invalid_site_nums=[1, 2], boson_fermion_mixing=False) @@ -425,7 +425,7 @@ def test_density_density_interaction(any_backend, np_random): if all(species1 == species2): # commutation relation tensor_commuted = tensors.permute_legs(tensor, codomain=[1, 0], domain=[2, 3]) - tensor_commuted.relabel({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) + tensor_commuted.labels.update({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) assert tensors.almost_equal(tensor_commuted, tensor) # check eigenvalues Nmax = site.Nmax if isinstance(site, sites.SpinlessBosonSite) else [1] * site.num_species @@ -493,7 +493,7 @@ def test_hopping(any_backend, np_random): # a_0_k^\dagger a_1_l + hc -> (exchange legs) -> -1 * a_0_l a_1_k^\dagger + hc # = a_1_k^\dagger a_0_l + hc = a_0_l^\dagger a_1_k + hc tensor_commuted = tensors.permute_legs(tensor, codomain=[1, 0], domain=[2, 3]) - tensor_commuted.relabel({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) + tensor_commuted.labels.update({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) assert tensors.almost_equal(tensor_commuted, tensor) check_coupling(couplings.hopping, site_num=2, invalid_site_nums=[1, 3], boson_fermion_mixing=True) @@ -547,7 +547,7 @@ def test_pairing(any_backend, np_random): if symmetric: # commutation relation tensor_commuted = tensors.permute_legs(tensor, codomain=[1, 0], domain=[2, 3]) - tensor_commuted.relabel({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) + tensor_commuted.labels.update({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) assert tensors.almost_equal(tensor_commuted, site.anti_commute_sign * tensor) check_coupling(couplings.pairing, site_num=2, invalid_site_nums=[1, 3], boson_fermion_mixing=True) @@ -614,7 +614,7 @@ def test_clock_clock_coupling(any_backend, np_random): npt.assert_almost_equal(tensors.trace(tensor).to_numpy(), 0) # commutation relation tensor_commuted = tensors.permute_legs(tensor, codomain=[1, 0], domain=[2, 3]) - tensor_commuted.relabel({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) + tensor_commuted.labels.update({'p0': 'p1', 'p1': 'p0', 'p0*': 'p1*', 'p1*': 'p0*'}) assert tensors.almost_equal(tensor_commuted, tensor) check_coupling(couplings.clock_clock_coupling, site_num=2, invalid_site_nums=[1, 3], boson_fermion_mixing=False) @@ -1153,8 +1153,8 @@ def test_coupling_permute_matches_direct_permute_legs(site_factory, label): } tensor_direct = permute_legs(coupling.to_tensor(), codomain=['p1', 'p0'], domain=['p1*', 'p0*'], levels=level_dict) # relabel to the same p{new_pos} convention Coupling.permute uses (site formerly at 1 is now p0) - tensor_direct = tensor_direct.relabel({'p0': 'q1', 'p1': 'q0', 'p0*': 'q1*', 'p1*': 'q0*'}) - tensor_direct = tensor_direct.relabel({'q0': 'p0', 'q1': 'p1', 'q0*': 'p0*', 'q1*': 'p1*'}) + tensor_direct.labels.update({'p0': 'q1', 'p1': 'q0', 'p0*': 'q1*', 'p1*': 'q0*'}) + tensor_direct.labels.update({'q0': 'p0', 'q1': 'p1', 'q0*': 'p0*', 'q1*': 'p1*'}) labels = ['p0', 'p1', 'p0*', 'p1*'] dim_permuted = permuted.to_tensor().to_numpy(labels, understood_braiding=True) diff --git a/tests/python_tests/test_planar.py b/tests/python_tests/test_planar.py index ac6e299a..c4e8a30c 100644 --- a/tests/python_tests/test_planar.py +++ b/tests/python_tests/test_planar.py @@ -141,8 +141,8 @@ def test_planar_combine_legs( num_domain_legs = T.num_domain_legs num_domain_flat_legs = T.num_domain_flat_legs for group in which_legs: - leg0 = T.get_leg_idcs(group[0])[0] - for i, leg in enumerate(T.get_leg_idcs(group)): + leg0 = T.labels.index(group[0]) + for i, leg in enumerate(T.labels.indices(group)): if i == 0: continue if leg < T.num_codomain_legs: @@ -168,7 +168,7 @@ def test_planar_combine_legs( # compare to nonplanar function T_combined2 = lasy.permute_legs(T, codomain=new_codomain, domain=new_domain, bend_right=bend_right) # make sure which_legs is sorted to avoid braids - which_legs = [sorted(T_combined2.get_leg_idcs(group)) for group in which_legs] + which_legs = [sorted(T_combined2.labels.indices(group)) for group in which_legs] T_combined2 = lasy.combine_legs(T_combined2, *which_legs, pipe_dualities=pipe_dualities) assert lasy.almost_equal(T_combined, T_combined2) @@ -248,8 +248,8 @@ def test_planar_contraction( B: lasy.SymmetricTensor = lasy.testing.random_tensor( symmetry, - codomain=[A._as_domain_leg(l) if A.has_label(l) else None for l in labels_B[0]], - domain=[A._as_codomain_leg(l) if A.has_label(l) else None for l in labels_B[1]], + codomain=[A._as_domain_leg(l) if A.labels.has(l) else None for l in labels_B[0]], + domain=[A._as_codomain_leg(l) if A.labels.has(l) else None for l in labels_B[1]], labels=[*labels_B[0], *reversed(labels_B[1])], backend=backend, np_random=np_random, @@ -298,7 +298,7 @@ def test_planar_eigh(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compati T2 = T2 + T2.hc T2_labels = list('efghijk')[:num_dom_legs] T2_labels.extend(reversed([f'{l}*' for l in T2_labels])) - T2.set_labels(T2_labels) + T2.labels.set(T2_labels) T2.test_sanity() left_bends = dom_cut @@ -318,14 +318,14 @@ def test_planar_eigh(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compati assert W.labels == ['b', 'c'] assert V.num_codomain_legs == cod_cut assert V.num_domain_legs == dom_cut + 1 - assert V.labels == [*T.labels[:cod_cut], 'a', *T.labels[T.num_legs - dom_cut :]] + assert V.labels == [*T.labels.as_list()[:cod_cut], 'a', *T.labels.as_list()[T.num_legs - dom_cut :]] assert lasy.planar.planar_almost_equal( lasy.planar_contraction(lasy.planar_contraction(V, W, 'a', 'b'), V.hc, 'c', 'a*'), T ) eye1 = lasy.SymmetricTensor.from_eye(T2.domain, T.backend, labels=T2_labels[: T2.num_domain_legs]) eye2 = lasy.SymmetricTensor.from_eye([V.get_leg('a')], T.backend, labels=['a']) - eye2_contr = [l for l in V.labels if l != 'a'] + eye2_contr = [l for l in V.labels.as_list() if l != 'a'] assert lasy.planar.planar_almost_equal(lasy.planar_contraction(V, V.hc, 'a', 'a*'), eye1) assert lasy.planar.planar_almost_equal( lasy.planar_contraction(V, V.hc, eye2_contr, [f'{l}*' for l in eye2_contr]), eye2 @@ -358,7 +358,7 @@ def test_planar_eig(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compatib T2: lasy.Tensor = make_compatible_tensor(T2.domain, T2.domain, cls=cls) T2_labels = list('efghijk')[:num_dom_legs] T2_labels.extend(reversed([f'{l}*' for l in T2_labels])) - T2.set_labels(T2_labels) + T2.labels.set(T2_labels) T2.test_sanity() left_bends = dom_cut @@ -378,7 +378,7 @@ def test_planar_eig(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compatib assert W.labels == ['b', 'c'] assert V.num_codomain_legs == cod_cut assert V.num_domain_legs == dom_cut + 1 - assert V.labels == [*T.labels[:cod_cut], 'a', *T.labels[T.num_legs - dom_cut :]] + assert V.labels == [*T.labels.as_list()[:cod_cut], 'a', *T.labels.as_list()[T.num_legs - dom_cut :]] W2, V2 = lasy.eig(T2, new_labels=['a', 'b', 'c'], new_leg_dual=new_leg_dual, sort=sort) W2.test_sanity() @@ -406,7 +406,7 @@ def test_planar_eigvals(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_comp T2: lasy.Tensor = make_compatible_tensor(T2.domain, T2.domain, cls=cls) T2_labels = list('efghijk')[:num_dom_legs] T2_labels.extend(reversed([f'{l}*' for l in T2_labels])) - T2.set_labels(T2_labels) + T2.labels.set(T2_labels) left_bends = dom_cut right_bends = cod - cod_cut @@ -524,8 +524,8 @@ def test_planar_partial_trace(codomain, domain, symmetry, backend, np_random): pairs = [(T.labels.index(l), T.labels.index(f'{l}*')) for l in trace_legs] res = lasy.planar.planar_partial_trace(T, *pairs) res.test_sanity() - assert res.labels == [l for l in T.labels if l[0] not in trace_legs] - assert res.legs == [T.get_leg(l) for l in T.labels if l[0] not in trace_legs] + assert res.labels == [l for l in T.labels.as_list() if l[0] not in trace_legs] + assert res.legs == [T.get_leg(l) for l in T.labels.as_list() if l[0] not in trace_legs] if T.symmetry.has_trivial_braid: T_np = T.to_numpy() @@ -594,12 +594,12 @@ def test_planar_permute_legs(J, K, codomain, domain, symmetry, backend, np_rando assert lasy.planar.planar_almost_equal(T, res) if codomain is None or len(codomain) == 0: - domain = T.get_leg_idcs(domain) + domain = T.labels.indices(domain) num_codom_legs = T.num_legs - len(domain) codomain = [i % T.num_legs for i in range(domain[0] + 1, domain[0] + 1 + num_codom_legs)] rev_domain = domain[::-1] else: - codomain = T.get_leg_idcs(codomain) + codomain = T.labels.indices(codomain) num_dom_legs = T.num_legs - len(codomain) rev_domain = [i % T.num_legs for i in range(codomain[-1] + 1, codomain[-1] + 1 + num_dom_legs)] leg_perm = [*codomain, *rev_domain] @@ -709,13 +709,13 @@ def test_planar_qr_lq(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compat R.test_sanity() assert Q.num_codomain_legs == cod_cut assert Q.num_domain_legs == dom_cut + 1 - assert Q.labels == [*T.labels[:cod_cut], 'v', *T.labels[T.num_legs - dom_cut :]] + assert Q.labels == [*T.labels.as_list()[:cod_cut], 'v', *T.labels.as_list()[T.num_legs - dom_cut :]] assert R.num_codomain_legs == 1 + (cod - cod_cut) assert R.num_domain_legs == dom - dom_cut - assert R.labels == ['v*', *T.labels[cod_cut : T.num_legs - dom_cut]] + assert R.labels == ['v*', *T.labels.as_list()[cod_cut : T.num_legs - dom_cut]] assert lasy.planar.planar_almost_equal(lasy.planar_contraction(Q, R, 'v', 'v*'), T) eye = lasy.SymmetricTensor.from_eye([Q.get_leg('v')], T.backend, labels=['v']) - contr = [l for l in Q.labels if l != 'v'] + contr = [l for l in Q.labels.as_list() if l != 'v'] assert lasy.planar.planar_almost_equal(lasy.planar_contraction(Q, Q.hc, contr, [f'{l}*' for l in contr]), eye) # compare to non-planar result idcs = [*range(T.num_legs - dom_cut, T.num_legs), *range(cod_cut)] @@ -733,13 +733,13 @@ def test_planar_qr_lq(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compat Q.test_sanity() assert L.num_codomain_legs == cod_cut assert L.num_domain_legs == dom_cut + 1 - assert L.labels == [*T.labels[:cod_cut], 'v*', *T.labels[T.num_legs - dom_cut :]] + assert L.labels == [*T.labels.as_list()[:cod_cut], 'v*', *T.labels.as_list()[T.num_legs - dom_cut :]] assert Q.num_codomain_legs == 1 + (cod - cod_cut) assert Q.num_domain_legs == dom - dom_cut - assert Q.labels == ['v', *T.labels[cod_cut : T.num_legs - dom_cut]] + assert Q.labels == ['v', *T.labels.as_list()[cod_cut : T.num_legs - dom_cut]] assert lasy.planar.planar_almost_equal(lasy.planar_contraction(L, Q, 'v*', 'v'), T) eye = lasy.SymmetricTensor.from_eye([Q.get_leg('v')], T.backend, labels='v') - contr = [l for l in Q.labels if l != 'v'] + contr = [l for l in Q.labels.as_list() if l != 'v'] assert lasy.planar.planar_almost_equal(lasy.planar_contraction(Q, Q.hc, contr, [f'{l}*' for l in contr]), eye) # compare to non-planar result L2, Q2 = lasy.lq(T2, new_labels=['v*', 'v'], new_leg_dual=new_leg_dual) @@ -776,11 +776,11 @@ def test_planar_svd(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compatib Vh.test_sanity() assert U.num_codomain_legs == cod_cut assert U.num_domain_legs == dom_cut + 1 - assert U.labels == [*T.labels[:cod_cut], 'a', *T.labels[T.num_legs - dom_cut :]] + assert U.labels == [*T.labels.as_list()[:cod_cut], 'a', *T.labels.as_list()[T.num_legs - dom_cut :]] assert S.labels == ['b', 'c'] assert Vh.num_codomain_legs == 1 + (cod - cod_cut) assert Vh.num_domain_legs == dom - dom_cut - assert Vh.labels == ['d', *T.labels[cod_cut : T.num_legs - dom_cut]] + assert Vh.labels == ['d', *T.labels.as_list()[cod_cut : T.num_legs - dom_cut]] assert isinstance(S, lasy.DiagonalTensor) assert (S >= 0).all() @@ -790,10 +790,10 @@ def test_planar_svd(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compatib lasy.planar_contraction(lasy.planar_contraction(U, S, 'a', 'b'), Vh, 'c', 'd'), T ) eye = lasy.SymmetricTensor.from_eye(S.domain, backend=T.backend, labels=['a*', 'a']) - contr_U = [l for l in U.labels if l != 'a'] - contr_Vh = [l for l in Vh.labels if l != 'd'] + contr_U = [l for l in U.labels.as_list() if l != 'a'] + contr_Vh = [l for l in Vh.labels.as_list() if l != 'd'] assert lasy.planar.planar_almost_equal(lasy.planar_contraction(U, U.hc, contr_U, [f'{l}*' for l in contr_U]), eye) - eye.set_labels(['d', 'd*']) + eye.labels.set(['d', 'd*']) assert lasy.planar.planar_almost_equal( lasy.planar_contraction(Vh, Vh.hc, contr_Vh, [f'{l}*' for l in contr_Vh]), eye ) @@ -834,11 +834,11 @@ def test_planar_svd(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compatib Vh.test_sanity() assert U.num_codomain_legs == cod_cut assert U.num_domain_legs == dom_cut + 1 - assert U.labels == [*T.labels[:cod_cut], 'a', *T.labels[T.num_legs - dom_cut :]] + assert U.labels == [*T.labels.as_list()[:cod_cut], 'a', *T.labels.as_list()[T.num_legs - dom_cut :]] assert S.labels == ['b', 'c'] assert Vh.num_codomain_legs == 1 + (cod - cod_cut) assert Vh.num_domain_legs == dom - dom_cut - assert Vh.labels == ['d', *T.labels[cod_cut : T.num_legs - dom_cut]] + assert Vh.labels == ['d', *T.labels.as_list()[cod_cut : T.num_legs - dom_cut]] norm_T = lasy.norm(T).to_numpy() lambda_kept = S / norm_T if normalize_to is None else (renormalize / norm_T) * S @@ -853,7 +853,7 @@ def test_planar_svd(cls, dom, cod, dom_cut, cod_cut, new_leg_dual, make_compatib assert lasy.planar.planar_almost_equal( lasy.planar_contraction(U, U.hc, contr_U, [f'{l}*' for l in contr_U]), eye ) - eye.set_labels(['d', 'd*']) + eye.labels.set(['d', 'd*']) assert lasy.planar.planar_almost_equal( lasy.planar_contraction(Vh, Vh.hc, contr_Vh, [f'{l}*' for l in contr_Vh]), eye ) @@ -935,7 +935,7 @@ def test_PlanarDiagram(symmetry, np_random): expect1 = lasy.planar.planar_contraction(expect1, mixL, 'wR', 'wL') expect1 = lasy.planar.planar_contraction(expect1, W.hc, ['p0*', 'wL*'], ['p', 'wR*']) expect1 = lasy.planar.planar_contraction(expect1, Lp.hc, ['vL*', 'wL*'], ['vR*', 'wR*']) - expect1 = expect1.relabel({'vR*': 'vL', 'vR': 'vL*'}) + expect1.labels.update({'vR*': 'vL', 'vR': 'vL*'}) expect1 = lasy.planar.planar_permute_legs(expect1, codomain=['p*', 'vL*']) assert expect1.labels == ['p*', 'vL*', 'vL', 'p'] expect1.test_sanity() @@ -968,7 +968,7 @@ def test_PlanarDiagram(symmetry, np_random): lasy.permute_legs(expect2, ['vR*', 'p', 'p*'], ['vL*', 'wL*'], bend_right=[False, None, None, True, None]), Lp.hc, ) - expect2 = expect2.relabel({'vR*': 'vL', 'vR': 'vL*'}) + expect2.labels.update({'vR*': 'vL', 'vR': 'vL*'}) expect2 = lasy.permute_legs(expect2, ['p*', 'vL*'], ['p', 'vL'], bend_right=[False, False, None, True]) expect2.test_sanity() assert lasy.planar.planar_almost_equal(res, expect2) diff --git a/tests/python_tests/test_spaces.py b/tests/python_tests/test_spaces.py index ab424fb0..2c16d34e 100644 --- a/tests/python_tests/test_spaces.py +++ b/tests/python_tests/test_spaces.py @@ -606,11 +606,11 @@ def test_DirectSumSpace_summand_labels(make_any_space, max_mult=3, max_sectors=3 # unlabeled by default d = spaces.DirectSumSpace([a, b, c]) d.test_sanity() - assert d.summand_labels == [None, None, None] + assert d.summand_labels == ['', '', ''] - d = spaces.DirectSumSpace([a, b, c], summand_labels=['IdL', None, 'IdR']) + d = spaces.DirectSumSpace([a, b, c], summand_labels=['IdL', '', 'IdR']) d.test_sanity() - assert d.summand_labels == ['IdL', None, 'IdR'] + assert d.summand_labels == ['IdL', '', 'IdR'] assert d.has_summand_label('IdL') assert not d.has_summand_label('other') assert d.get_summand_idx('IdL') == 0 @@ -623,11 +623,11 @@ def test_DirectSumSpace_summand_labels(make_any_space, max_mult=3, max_sectors=3 # duplicate labels are rejected with pytest.raises(ValueError): - spaces.DirectSumSpace([a, b, c], summand_labels=['x', 'x', None]) + spaces.DirectSumSpace([a, b, c], summand_labels=['x', 'x', '']) # length mismatch is rejected with pytest.raises(ValueError): - spaces.DirectSumSpace([a, b, c], summand_labels=['x', None]) + spaces.DirectSumSpace([a, b, c], summand_labels=['x', '']) # structural equality ignores labels unlabeled = spaces.DirectSumSpace([a, b, c]) @@ -635,13 +635,13 @@ def test_DirectSumSpace_summand_labels(make_any_space, max_mult=3, max_sectors=3 # labels are preserved through structure-preserving ops dual = d.dual - assert dual.summand_labels == ['IdL', None, 'IdR'] + assert dual.summand_labels == ['IdL', '', 'IdR'] opp = d.with_opposite_duality() - assert opp.summand_labels == ['IdL', None, 'IdR'] + assert opp.summand_labels == ['IdL', '', 'IdR'] # nested DirectSumSpace summands propagate their own labels nested_inner = spaces.DirectSumSpace([b, c], summand_labels=['x', 'y']) - nested = spaces.DirectSumSpace([a, nested_inner], summand_labels=['IdL', None]) + nested = spaces.DirectSumSpace([a, nested_inner], summand_labels=['IdL', '']) assert nested.spaces == [a, b, c] assert nested.summand_labels == ['IdL', 'x', 'y'] diff --git a/tests/python_tests/test_sparse.py b/tests/python_tests/test_sparse.py index 4a16e006..092f7842 100644 --- a/tests/python_tests/test_sparse.py +++ b/tests/python_tests/test_sparse.py @@ -251,8 +251,8 @@ def run(): ) if charged.num_legs == 1 and len(vec.legs) > 1: charged = tensors.split_legs(charged, 0) - if vec.labels is not None and any(l is not None for l in vec.labels): - charged.set_labels(list(vec.labels)) + if any(vec.labels.as_list()): + charged.labels.set(vec.labels.as_list()) op2, cflat = sparse.NumpyArrayLinearOperator.from_matvec_and_vector(lasyten_matvec, charged) npt.assert_allclose(op2.matvec(cflat), op2.tensor_to_flat_array(lasyten_matvec(charged))) diff --git a/tests/python_tests/test_tensors.py b/tests/python_tests/test_tensors.py index 9dd88dc4..fb55a988 100644 --- a/tests/python_tests/test_tensors.py +++ b/tests/python_tests/test_tensors.py @@ -81,25 +81,25 @@ def test_base_Tensor(make_compatible_space, compatible_backend): print('checking different labels input formats') tens1 = DummyTensor([a, b, c], [d, e], backend=compatible_backend, labels=None, dtype=Dtype.float64) tens1.test_sanity() - assert tens1._labels == [None] * 5 + assert tens1.labels == [''] * 5 tens2 = DummyTensor( [a, b, c], [d, e], backend=compatible_backend, labels=['a', 'b', 'c', 'e*', 'd*'], dtype=Dtype.float64 ) tens2.test_sanity() - assert tens2._labels == ['a', 'b', 'c', 'e*', 'd*'] + assert tens2.labels == ['a', 'b', 'c', 'e*', 'd*'] tens3 = DummyTensor( - [a, b, c], [d, e], backend=compatible_backend, labels=[['a', None, None], ['d*', 'e*']], dtype=Dtype.float64 + [a, b, c], [d, e], backend=compatible_backend, labels=[['a', '', ''], ['d*', 'e*']], dtype=Dtype.float64 ) tens3.test_sanity() - assert tens3._labels == ['a', None, None, 'e*', 'd*'] + assert tens3.labels == ['a', '', '', 'e*', 'd*'] tens4 = DummyTensor( - [a, b, c], [d, e], backend=compatible_backend, labels=[['a', None, None], None], dtype=Dtype.float64 + [a, b, c], [d, e], backend=compatible_backend, labels=[['a', '', ''], None], dtype=Dtype.float64 ) tens4.test_sanity() - assert tens4._labels == ['a', None, None, None, None] + assert tens4.labels == ['a', '', '', '', ''] print('checking .legs , .num_(co)domain_legs') for t in [tens1, tens2, tens3, tens4]: @@ -118,28 +118,28 @@ def test_base_Tensor(make_compatible_space, compatible_backend): _ = tens1 == tens2 print('checking .(co)domain_labels') - assert tens1.codomain_labels == [None] * 3 - assert tens1.domain_labels == [None] * 2 + assert tens1.codomain_labels == [''] * 3 + assert tens1.domain_labels == [''] * 2 assert tens2.codomain_labels == ['a', 'b', 'c'] assert tens2.domain_labels == ['d*', 'e*'] - assert tens3.codomain_labels == ['a', None, None] + assert tens3.codomain_labels == ['a', '', ''] assert tens3.domain_labels == ['d*', 'e*'] - assert tens4.codomain_labels == ['a', None, None] - assert tens4.domain_labels == [None, None] + assert tens4.codomain_labels == ['a', '', ''] + assert tens4.domain_labels == ['', ''] print('checking .is_fully_labelled') - assert not tens1.is_fully_labelled - assert tens2.is_fully_labelled - assert not tens3.is_fully_labelled - assert not tens4.is_fully_labelled + assert not tens1.labels.is_fully_labelled + assert tens2.labels.is_fully_labelled + assert not tens3.labels.is_fully_labelled + assert not tens4.labels.is_fully_labelled print('check setting labels') tens1.labels = ['e', 'b', 'f', 'c', 'x'] - assert tens1._labels == ['e', 'b', 'f', 'c', 'x'] + assert tens1.labels == ['e', 'b', 'f', 'c', 'x'] print('check relabel') - tens1.relabel(dict(e='xx', x='e')) - assert tens1._labels == ['xx', 'b', 'f', 'c', 'e'] + tens1.labels.update(dict(e='xx', x='e')) + assert tens1.labels == ['xx', 'b', 'f', 'c', 'e'] print('check _parse_leg_idx') assert tens1._parse_leg_idx(1) == (False, 1, 1) @@ -155,15 +155,15 @@ def test_base_Tensor(make_compatible_space, compatible_backend): assert tens2.get_leg('e*') == e.dual print('check has_label') - assert tens2.has_label('a') - assert tens2.has_label('a', 'b', 'e*') - assert not tens2.has_label('foo') - assert not tens2.has_label('a', 'b', '42') + assert tens2.labels.has('a') + assert tens2.labels.has('a', 'b', 'e*') + assert not tens2.labels.has('foo') + assert not tens2.labels.has('a', 'b', '42') print('check labels_are') - assert tens2.labels_are('a', 'c', 'b', 'e*', 'd*') - assert tens2.labels_are('c', 'e*', 'd*', 'a', 'b', planar=True) - assert not tens2.labels_are('a', 'c', 'b', 'e*', 'd*', planar=True) + assert tens2.labels.are('a', 'c', 'b', 'e*', 'd*') + assert tens2.labels.are('c', 'e*', 'd*', 'a', 'b', planar=True) + assert not tens2.labels.are('a', 'c', 'b', 'e*', 'd*', planar=True) @pytest.mark.parametrize('leg_nums', [(1, 1), (2, 1), (3, 0), (0, 3)], ids=['1->1', '1->2', '0->3', '3->0']) @@ -909,7 +909,7 @@ def test_ChargedTensor_to_dense_block_single_sector( ): sector = make_compatible_sectors(1)[0] charge_leg = ElementarySpace(compatible_symmetry, SectorArray.from_sector(sector)) - inv_part = make_compatible_tensor(codomain=1, domain=[charge_leg], labels=[None, '!']) + inv_part = make_compatible_tensor(codomain=1, domain=[charge_leg], labels=['', '!']) tens = ChargedTensor(inv_part, charged_state=[1]) leg = tens.codomain[0] @@ -1419,13 +1419,13 @@ def test_add_trivial_leg(cls, domain, codomain, is_dual, make_compatible_tensor, def test_add_trivial_leg_rejects_duplicate_label(make_compatible_tensor): - tens = make_compatible_tensor(codomain=1, domain=1, labels=['existing', None]) + tens = make_compatible_tensor(codomain=1, domain=1, labels=['existing', '']) with pytest.raises(ValueError, match='already has label'): tensors.add_trivial_leg(tens, label='existing') res = tensors.add_trivial_leg(tens, label='new') - assert res.labels == ['new', 'existing', None] + assert res.labels == ['new', 'existing', ''] def test_tensor_constructor_rejects_duplicate_labels(make_compatible_tensor): @@ -2155,7 +2155,7 @@ def test_eigh(cls, dom, new_leg_dual, make_compatible_tensor): T: Tensor = make_compatible_tensor(dom, dom, cls=cls) T: Tensor = make_compatible_tensor(T.domain, T.domain, cls=cls) T = T + T.hc - T.set_labels(list('efghijk')[: 2 * dom]) + T.labels.set(list('efghijk')[: 2 * dom]) T.test_sanity() new_leg_dual_arg = {'new_leg_dual': True} if new_leg_dual else {} @@ -2184,7 +2184,7 @@ def test_eigh(cls, dom, new_leg_dual, make_compatible_tensor): def test_eig(cls, dom, new_leg_dual, make_compatible_tensor): T: Tensor = make_compatible_tensor(dom, dom, cls=cls) T: Tensor = make_compatible_tensor(T.domain, T.domain, cls=cls) - T.set_labels(list('efghijk')[: 2 * dom]) + T.labels.set(list('efghijk')[: 2 * dom]) T.test_sanity() new_leg_dual_arg = {'new_leg_dual': True} if new_leg_dual else {} @@ -2212,7 +2212,7 @@ def test_eigvalsh(cls, dom, new_leg_dual, make_compatible_tensor): T: Tensor = make_compatible_tensor(dom, dom, cls=cls) T: Tensor = make_compatible_tensor(T.domain, T.domain, cls=cls) T = T + T.hc - T.set_labels(list('efghijk')[: 2 * dom]) + T.labels.set(list('efghijk')[: 2 * dom]) T.test_sanity() new_leg_dual_arg = {'new_leg_dual': True} if new_leg_dual else {} @@ -2236,7 +2236,7 @@ def test_eigvalsh(cls, dom, new_leg_dual, make_compatible_tensor): def test_eigvals(cls, dom, new_leg_dual, make_compatible_tensor): T: Tensor = make_compatible_tensor(dom, dom, cls=cls) T: Tensor = make_compatible_tensor(T.domain, T.domain, cls=cls) - T.set_labels(list('efghijk')[: 2 * dom]) + T.labels.set(list('efghijk')[: 2 * dom]) T.test_sanity() new_leg_dual_arg = {'new_leg_dual': True} if new_leg_dual else {} @@ -2263,7 +2263,7 @@ def test_slice_leg(cls, dom, new_leg_dual, make_compatible_tensor, compatible_ba T: Tensor = make_compatible_tensor(dom, dom, cls=cls, **kwargs) T: Tensor = make_compatible_tensor(T.domain, T.domain, cls=cls, **kwargs) T = T + T.hc - T.set_labels(list('efghijk')[: 2 * dom]) + T.labels.set(list('efghijk')[: 2 * dom]) W, V = tensors.eigh(T, new_labels=['a', 'b', 'c'], new_leg_dual=new_leg_dual) eval_leg = V.domain_labels[0] @@ -2494,10 +2494,10 @@ def test_horizontal_factorization(trunc, make_compatible_tensor, compatible_symm B.test_sanity() assert A.num_codomain_legs == cod_cut assert A.num_domain_legs == dom_cut + 1 - assert A.labels == T.labels[:cod_cut] + ['a'] + T.labels[-dom_cut:] + assert A.labels == T.labels.as_list()[:cod_cut] + ['a'] + T.labels.as_list()[-dom_cut:] assert B.num_codomain_legs == 1 + (cod - cod_cut) assert B.num_domain_legs == dom - dom_cut - assert B.labels == ['b'] + T.labels[cod_cut:-dom_cut] + assert B.labels == ['b'] + T.labels.as_list()[cod_cut:-dom_cut] if T.symmetry.can_be_dropped: A_np = A.to_numpy(understood_braiding=True) # [*J1, a, *K1_rev] @@ -2966,14 +2966,14 @@ def test_partial_compose(cls_A, cls_B, legs_A, legs_B, A_contr_leg, make_compati if isinstance(B, Mask): expect = tensors.apply_mask(A, B, A_contr_leg) - expect.relabel({A.labels[A_contr_leg]: B.labels[int(A_contr_leg >= A.num_codomain_legs)]}) - expect.relabel(relabel1) - expect.relabel(relabel2) + expect.labels.update({A.labels[A_contr_leg]: B.labels[int(A_contr_leg >= A.num_codomain_legs)]}) + expect.labels.update(relabel1) + expect.labels.update(relabel2) elif isinstance(B, DiagonalTensor): expect = tensors.scale_axis(A, B, A_contr_leg) - expect.relabel({A.labels[A_contr_leg]: B.labels[int(A_contr_leg >= A.num_codomain_legs)]}) - expect.relabel(relabel1) - expect.relabel(relabel2) + expect.labels.update({A.labels[A_contr_leg]: B.labels[int(A_contr_leg >= A.num_codomain_legs)]}) + expect.labels.update(relabel1) + expect.labels.update(relabel2) else: # we can treat the invariant part of ChargedTensor in the same way, but need to take some extra care for the charge leg A_ = A.invariant_part if isinstance(A, ChargedTensor) else A @@ -3028,7 +3028,8 @@ def test_partial_compose(cls_A, cls_B, legs_A, legs_B, A_contr_leg, make_compati elif isinstance(A, ChargedTensor): expect = ChargedTensor.from_invariant_part(expect, A.charged_state) elif isinstance(B, ChargedTensor): - expect = tensors.move_leg(expect, f'{c}B', domain_pos=0, bend_right=True).relabel({f'{c}B': c}) + expect = tensors.move_leg(expect, f'{c}B', domain_pos=0, bend_right=True) + expect.labels.update({f'{c}B': c}) expect = ChargedTensor.from_invariant_part(expect, B.charged_state) assert res.labels == expect.labels @@ -3147,7 +3148,7 @@ def test_partial_trace(cls, codom, dom, make_compatible_space, make_compatible_t else: assert isinstance(res, cls) res.test_sanity() - assert res.labels == [l for l in T.labels if l[0] not in trace_legs] + assert res.labels == [l for l in T.labels.as_list() if l[0] not in trace_legs] assert res.codomain.factors == [ sp for sp, l in zip(T.codomain, T.codomain_labels) if l[0] not in trace_legs ] @@ -3804,8 +3805,8 @@ def test_tdot( # create B such that legs with the same label can be contracted B: Tensor = make_compatible_tensor( - codomain=[A._as_domain_leg(l) if A.has_label(l) else None for l in labels_B[0]], - domain=[A._as_codomain_leg(l) if A.has_label(l) else None for l in labels_B[1]], + codomain=[A._as_domain_leg(l) if A.labels.has(l) else None for l in labels_B[0]], + domain=[A._as_codomain_leg(l) if A.labels.has(l) else None for l in labels_B[1]], labels=[*labels_B[0], *reversed(labels_B[1])], max_block_size=2, max_blocks=3, @@ -3828,8 +3829,8 @@ def test_tdot( expect_legs = [A.get_leg(n) for n in range(A.num_legs) if n not in contr_A] + [ B.get_leg(n) for n in range(B.num_legs) if n not in contr_B ] - expect_labels = [A._labels[n] for n in range(A.num_legs) if n not in contr_A] + [ - B._labels[n] for n in range(B.num_legs) if n not in contr_B + expect_labels = [A.labels[n] for n in range(A.num_legs) if n not in contr_A] + [ + B.labels[n] for n in range(B.num_legs) if n not in contr_B ] if (cls_A is Mask and cls_B is Mask) and num_contr > 0: @@ -4026,7 +4027,7 @@ def test_tensor_grid_cell_roundtrip(make_compatible_tensor, make_compatible_spac # The operation only depends on the current legs, not tensor_from_grid provenance. direct = make_compatible_tensor([res.codomain[0]], [res.domain[-1]], cls=SymmetricTensor, use_pipes=False) - direct.set_labels(['wL', 'wR']) + direct.labels.set(['wL', 'wR']) expected = tensors.apply_mask( direct, direct.get_leg('wL').projection_onto_summand(1, backend=direct.backend), @@ -4207,7 +4208,7 @@ def test_tensor_from_grid_hidden_labels(make_compatible_tensor, np_random): result = tensors.tensor_from_grid([[T]], labels=labels) assert isinstance(result, HiddenLegTensor) - assert all(result.labels == labels) + assert result.labels == labels assert result.hidden_leg_idcs() == [i for i, l in enumerate(labels) if l.startswith('!')] @@ -4452,7 +4453,7 @@ def test_HiddenLegTensor_implicit_dual_contraction(make_compatible_tensor): res.test_sanity() # Hidden duals contract implicitly; only public open legs remain. assert '!h' not in (res.labels or []) and '!h*' not in (res.labels or []) - assert set(lab for lab in res.labels if lab) == {'a', 'c'} + assert set(lab for lab in res.labels.as_list() if lab) == {'a', 'c'} if isinstance(res, HiddenLegTensor): assert not res.hidden_leg_idcs() @@ -4494,7 +4495,7 @@ def test_HiddenLegTensor_add_squeeze_trivial_leg_keeps_hidden(pos, make_compatib res = tensors.add_trivial_leg(H, label='t', **pos) res.test_sanity() assert isinstance(res, HiddenLegTensor) - assert [l for l in res.labels if l != 't'] == H.labels + assert [l for l in res.labels.as_list() if l != 't'] == H.labels assert [res.labels[i] for i in res.hidden_leg_idcs()] == ['!h'] assert tensors.almost_equal(res.unhide_legs(), tensors.add_trivial_leg(T, label='t', **pos)) @@ -4842,7 +4843,8 @@ def test_flatten_pipe_leg(dual_pipes, make_compatible_tensor, make_compatible_sp def combine(T): dualities = [True, False] if dual_pipes else [False, True] T = tensors.combine_legs(T, ['vL0', 'vL1'], ['vR1', 'vR0'], pipe_dualities=dualities) - return T.relabel({'(vL0.vL1)': 'vL', '(vR1.vR0)': 'vR'}) + T.labels.update({'(vL0.vL1)': 'vL', '(vR1.vR0)': 'vR'}) + return T A, B = [combine(T) for T in [A0, B0]] assert A.codomain == A.domain diff --git a/toycodes/tenpy_toycodes/a_mps.py b/toycodes/tenpy_toycodes/a_mps.py index 8352e8e1..6d6e15dc 100644 --- a/toycodes/tenpy_toycodes/a_mps.py +++ b/toycodes/tenpy_toycodes/a_mps.py @@ -245,6 +245,6 @@ def split_truncate_theta(theta, chi_max, eps): """ A, S, B, _, _ = lasy.truncated_svd(theta, ['vR', 'vL'], chi_max=chi_max, svd_min=eps) B = lasy.permute_legs(B, codomain=['vL', 'p1'], bend_right=True) - A.relabel({'p0': 'p'}) - B.relabel({'p1': 'p'}) + A.labels.update({'p0': 'p'}) + B.labels.update({'p1': 'p'}) return A, S, B diff --git a/toycodes/tenpy_toycodes/d_dmrg.py b/toycodes/tenpy_toycodes/d_dmrg.py index 020ebd91..f39dcd33 100644 --- a/toycodes/tenpy_toycodes/d_dmrg.py +++ b/toycodes/tenpy_toycodes/d_dmrg.py @@ -60,11 +60,11 @@ def __init__(self, LP, RP, W1, W2): self.W1 = lasy.permute_legs( W1, ['p', 'wR'], ['wL', 'p*'], bend_right=[False, None, True, None] ) # wL i wC i* -> i wC i* wL - self.W1.relabel({'p': 'p0', 'p*': 'p0*', 'wR': 'wC'}) + self.W1.labels.update({'p': 'p0', 'p*': 'p0*', 'wR': 'wC'}) self.W2 = lasy.permute_legs( W2, ['p', 'wR'], ['wL', 'p*'], bend_right=[False, None, True, None] ) # wC j wR j* -> j wR j* wC - self.W2.relabel({'p': 'p1', 'p*': 'p1*', 'wL': 'wC'}) + self.W2.labels.update({'p': 'p1', 'p*': 'p1*', 'wL': 'wC'}) def matvec(self, theta: lasy.Tensor) -> lasy.Tensor: """Calculate |theta'> = H_eff |theta>""" From aa6b598057403ccec315eefa08e81e727b749ddf Mon Sep 17 00:00:00 2001 From: Johannes Hauschild Date: Wed, 28 Oct 2026 16:18:08 +0100 Subject: [PATCH 2/2] add explicit test for label API --- tests/python_tests/test_tensors.py | 50 +++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tests/python_tests/test_tensors.py b/tests/python_tests/test_tensors.py index fb55a988..8c2daa08 100644 --- a/tests/python_tests/test_tensors.py +++ b/tests/python_tests/test_tensors.py @@ -29,8 +29,9 @@ SymmetryError, TensorProduct, ) -from lasyten.tensors import ChargedTensor, DiagonalTensor, HiddenLegTensor, Mask, SymmetricTensor, Tensor +from lasyten.tensors import ChargedTensor, DiagonalTensor, HiddenLegTensor, LegLabels, Mask, SymmetricTensor, Tensor from lasyten.testing import assert_equivalent_legs, assert_tensors_almost_equal, random_tensor, swap_gate_numpy +from lasyten.tools import hdf5_io from lasyten.tools.misc import duplicate_entries, inverse_permutation, iter_common_noncommon_sorted_arrays, to_valid_idx fermion_parity = FermionParity().as_Symmetry() @@ -166,6 +167,53 @@ def test_base_Tensor(make_compatible_space, compatible_backend): assert not tens2.labels.are('a', 'c', 'b', 'e*', 'd*', planar=True) +def test_LegLabels_api(make_compatible_space, compatible_backend, tmp_path): + """Exercise the composed LegLabels API (list-like access, rename, HDF5).""" + ll = LegLabels(['a', '', 'c']) + assert isinstance(ll, LegLabels) + assert ll == ['a', '', 'c'] + assert len(ll) == 3 + assert ll[0] == 'a' and ll[1] == '' + assert ll.as_list() == ['a', '', 'c'] + + ll[1] = 'b' + ll['a'] = 'a0' + assert ll.as_list() == ['a0', 'b', 'c'] + assert ll.index('b') == 1 + assert ll.index(-1) == 2 + assert ll.indices(['c', 0]) == [2, 0] + assert 'b' in ll + assert ll.has('b', 'c') + assert not ll.has('missing') + + ll.update({'b': 'bb'}) + assert ll[1] == 'bb' + ll.set(['x', 'y', 'z']) + assert ll == ['x', 'y', 'z'] + + a, b = make_compatible_space(), make_compatible_space() + t = DummyTensor([a], [b], backend=compatible_backend, labels=['p', 'p*'], dtype=Dtype.float64) + assert isinstance(t.labels, LegLabels) + assert t.labels == ['p', 'p*'] + t.labels = ['x', 'x*'] + assert t.labels.as_list() == ['x', 'x*'] + t.labels[0] = 'y' + assert t.labels[0] == 'y' + t.labels['y'] = 'z' + assert t.labels.as_list() == ['z', 'x*'] + t.labels.update({'z': 'p', 'x*': 'p*'}) + assert t.labels == ['p', 'p*'] + + path = tmp_path / 'leg_labels.hdf5' + h5py = pytest.importorskip('h5py') + with h5py.File(path, 'w') as f: + hdf5_io.save_to_hdf5(f, ll) + with h5py.File(path, 'r') as f: + loaded = hdf5_io.load_from_hdf5(f) + assert isinstance(loaded, LegLabels) + assert loaded.as_list() == ll.as_list() + + @pytest.mark.parametrize('leg_nums', [(1, 1), (2, 1), (3, 0), (0, 3)], ids=['1->1', '1->2', '0->3', '3->0']) @pytest.mark.parametrize('use_pipes', [True, 0.3]) def test_SymmetricTensor(make_compatible_tensor, leg_nums, use_pipes):