Skip to content

Resolve spaces diamonds (AbelianLegPipe + ElementarySpace) - #323

Open
jhauschild wants to merge 12 commits into
mainfrom
refactor_AbelianLegPipe_diamond_inheritance
Open

jhauschild wants to merge 12 commits into
mainfrom
refactor_AbelianLegPipe_diamond_inheritance

Conversation

@jhauschild

Copy link
Copy Markdown
Member

Context

#254 listed three MI uses; #310 and #317 already removed Tensor-labels and Site-DOF diamonds. Remaining item 3 is two related diamonds that both block nanobind (#255):

flowchart TB
  LegOrSpace --> Leg
  LegOrSpace --> Space
  Leg --> LegPipe
  Leg --> ElementarySpace
  Space --> ElementarySpace
  LegPipe --> AbelianLegPipe
  ElementarySpace --> AbelianLegPipe
Loading

Intentional product tension:

  • AbelianBackend / TeNPy: combined legs act as one charge space → need ElementarySpace.
  • FusionTreeBackend: pipes are nesting over flat legs, not elementary spaces → need plain LegPipe.

Nanobind binds only one C++ base and assumes address-preserving non-virtual inheritance. Both diamonds must go.

Why we do not keep LegOrSpace

Today LegOrSpace exists for one reason: a shared enable_shared_from_this so ElementarySpace : Space, Leg has a single control block. That reason disappears once ES no longer multiply-inherits.

A common root would only still be useful as a polymorphic factor type (vector<shared_ptr<Base>> holding both ES and LegPipe). That is a bad fit here:

  • If Space sits under that root, TensorProduct is also under it — then factors need runtime is_leg() filters to exclude TP.
  • You cannot put only ES and LegPipe under a common root without either MI (ES : Space, Axis) or making ES not a Space.
  • So the common root does not model the real type relation; it is just an open polymorphic bag.

Decision: delete LegOrSpace. Use two separate trees, each with its own enable_shared_from_this. Represent “tensor axis = ES | LegPipe” as an explicit closed sum, not a shared base.

Target hierarchy

flowchart TB
  Space["Space enable_shared_from_this"]
  LegPipe["LegPipe enable_shared_from_this"]
  Space --> ElementarySpace
  Space --> TensorProduct
  ElementarySpace --> DirectSumSpace
  ElementarySpace --> AbelianLegPipe
Loading

Two independent trees. No virtual inheritance. No shared root.

Type Space? Leg (axis)? Pipe?
ElementarySpace / DSS yes (C++) yes (ABC / handle) no
AbelianLegPipe yes yes yes (composed)
TensorProduct yes no no
LegPipe no yes yes (plain)

Axis handle (replaces Leg::Ptr)

// conceptual
using AxisPtr = std::variant<ElementarySpace::Ptr, LegPipe::Ptr>;
// or a thin value type `Leg` wrapping that variant with helpers:
//   symmetry(), dim(), is_dual(), dual(), is_pipe(), flat_legs(), ...

Prefer a thin Leg handle (value type wrapping the variant) so call sites keep leg->is_dual / leg.dual() sugar without an inheritance base. Python still sees concrete ElementarySpace / LegPipe instances; the handle is a C++ ergonomics layer (bindings continue to pass the concrete object).

Shared axis operations that today are virtual on Leg become:

  • methods on each concrete type, plus
  • free functions / handle methods that visit the variant

TensorProduct::factors becomes vector<AxisPtr> (or vector<Leg> handle). Construction asserts the variant alternatives only — TP cannot sneak in by inheritance.

Delete abstract C++ base Leg

Today's abstract Leg goes away as a polymorphic base:

  • ES absorbs former Leg fields it needs (is_dual, basis_perm, ascii_arrow, …)
  • LegPipe keeps its own copies of axis fields (symmetry, dim, is_dual, …)
  • Dedupe: ES no longer maintains parallel Space::symmetry/dim and Leg::symmetry/dim

Part 1 — AbelianLegPipe composition

Make AbelianLegPipe : public ElementarySpace only, composing former LegPipe fields:

  • Keep members: legs, num_legs, combine_cstyle, sector_strides, fusion_outcomes_sort, block_ind_map_slices, block_ind_map
  • Keep public name AbelianLegPipe
  • AbelianBackend::make_pipe → ALP (still ES → nested pipes + Site.leg OK)
  • Default / FusionTree make_pipe → plain LegPipe

isinstance(alp, LegPipe) becomes False. Use is_pipe / lasy.is_leg_pipe.

Part 2 — Linearize ElementarySpace / drop LegOrSpace

C++ shape

  1. Delete LegOrSpace and abstract base Leg.
  2. Space : enable_shared_from_this<Space> — sector API; owns symmetry / dim.
  3. ElementarySpace : public Space only — axis fields from former Leg; single dual() / operator==.
  4. LegPipe : enable_shared_from_this<LegPipe> — independent tree; nested legs as vector<AxisPtr>; as_space() → TensorProduct.
  5. TensorProduct : public Space — factors: vector<AxisPtr>; not an axis alternative.
  6. DirectSumSpace / AbelianLegPipe — ES subclasses as today / Part 1.

Python Leg ABC

# register ElementarySpace and LegPipe only — not TensorProduct
class Leg(abc.ABC):
    ...

Keeps isinstance(es, Leg) for asserting.py. isinstance(x, Space) remains real C++ inheritance.

Bindings

  • class_<Space> / class_<ElementarySpace, Space> / class_<AbelianLegPipe, ElementarySpace> / …
  • class_<LegPipe> as its own top (no Space/LegOrSpace base)
  • No bound MI; no virtual bases

Trampolines

  • Drop MI dual hooks (dual_leg/dual_space), dual operator==, as_space_obj rename, init_leg.
  • Separate trampolines for Space-chain vs LegPipe.

Pipe protocol

On concrete types + axis handle helpers:

  • is_pipe() — true for plain LegPipe and AbelianLegPipe
  • is_plain_leg_pipe := pipe and not ES / not abelian
  • is_abelian_leg_pipe() — ALP

Python: lasy.is_leg_pipe(x) covers both pipe kinds.

Mandatory dispatch updates

Cluster Files Change
combine/split/flatten ops_legs.cpp visit/is_pipe; ALP strips to plain ES when flattening
Mask / Diagonal / slice mask.cpp, diagonal_tensor.cpp, ops_legs reject any pipe, not only LegPipe cast
swap/twist spaces.cpp ES path for ALP; recurse only plain pipes
FusionTree fusion_tree_backend.cpp plain LegPipe only
Site guards degrees_of_freedom.cpp ALP as ES OK; reject plain LegPipe
Factors / get_leg spaces.h, tensor.h AxisPtr / Leg handle
HDF5 hdf5_export.cpp concrete load/save; ALP under ES path
asserting / tests asserting.py Leg ABC; abelian is_pipe not isinstance(LegPipe)

isinstance guarantees after

Check Result
isinstance(es/dss/alp, Space) True (C++)
isinstance(es/dss/alp/pipe, Leg) True (Python ABC)
isinstance(tp, Space) True
isinstance(tp, Leg) False
isinstance(pipe, Space) False
isinstance(alp, LegPipe) False (use is_pipe)
isinstance(alp, ElementarySpace) True

Sequencing

  1. Part 1 — ALP composition (outer diamond) + is_pipe protocol.
  2. Part 2 — Drop Leg/LegOrSpace; introduce axis handle; migrate Leg::Ptr call sites; Python Leg ABC.
  3. Conversion docs (LegLabels / SiteDOF style).
  4. Nanobind (port from pybind11 to nanobind #255).

Call-site map (docs)

Old New
LegOrSpace shared root deleted
AbelianLegPipe MRO includes LegPipe AbelianLegPipe → ElementarySpace → Space
ElementarySpace MRO includes C++ Leg ElementarySpace → Space; isinstance(..., Leg) via ABC
Leg::Ptr factors AxisPtr / thin Leg handle (ES | LegPipe)
isinstance(x, LegPipe) for abelian combines is_pipe / isinstance(x, AbelianLegPipe)
dynamic_pointer_cast<Leg>(…) visit handle / cast to ES or LegPipe
Dual dual_leg / dual_space hooks single dual() per concrete type

Remove spaces diamond MI for nanobind readiness (#254).
AbelianLegPipe is ElementarySpace-only with composed pipe fields;
drop LegOrSpace and make Leg a value handle over ES|LegPipe.
Python Leg is an ABC; use is_leg_pipe() instead of isinstance(..., LegPipe) for ALP.
@jhauschild

Copy link
Copy Markdown
Member Author

This branch contains the current main branch and #310 and #317 already merged into it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant