From d37c673944e204bcdeeb6167b645f416bc28a7d3 Mon Sep 17 00:00:00 2001 From: julpe Date: Fri, 3 Jul 2026 15:36:12 +0200 Subject: [PATCH] Changes for memory efficiency and streamlined deepcopy into separate copy method. Also fixed Eliashberg equation for multi-orbital systems. --- .github/dependabot.yml | 4 + .github/workflows/CI.yml | 6 +- .github/workflows/Docs.yml | 42 ++ .github/workflows/Lint.yml | 38 ++ .typos.toml | 3 + CONTRIBUTING.md | 14 +- README.md | 2 +- codecov.yml | 2 +- dgamore/DGAmore.py | 127 +++-- dgamore/__init__.py | 3 +- dgamore/brillouin_zone.py | 4 +- dgamore/bubble_gen.py | 14 +- dgamore/config.py | 12 +- dgamore/config_parser.py | 7 +- dgamore/dga_config.yaml | 13 +- dgamore/dga_io.py | 2 +- dgamore/dga_logger.py | 2 +- dgamore/dmft_interface.py | 2 +- dgamore/eliashberg_solver.py | 478 ++++++++++++++---- dgamore/four_point.py | 103 +++- dgamore/gap_function.py | 2 +- dgamore/greens_function.py | 25 +- dgamore/hamiltonian.py | 18 +- dgamore/interaction.py | 32 +- dgamore/lambda_correction.py | 2 +- dgamore/local_four_point.py | 97 ++-- dgamore/local_n_point.py | 13 +- dgamore/local_sde.py | 58 +-- dgamore/local_two_point.py | 6 +- dgamore/matsubara_frequencies.py | 2 +- dgamore/max_ent.py | 35 +- dgamore/memory_estimator.py | 232 ++++++--- dgamore/mpi_utils.py | 46 +- dgamore/n_point_base.py | 48 +- dgamore/nonlocal_sde.py | 377 ++++++++++---- dgamore/plotting.py | 4 +- dgamore/self_energy.py | 17 +- dgamore/symmetrize_new.py | 8 +- dgamore/symmetry_reduction.py | 17 +- dgamore/two_point.py | 6 +- docs/conf.py | 2 +- docs/configuration.rst | 51 +- docs/contributing.rst | 14 +- docs/installation.rst | 4 +- docs/usage.rst | 14 +- pyproject.toml | 12 +- setup.py | 7 +- tests/__init__.py | 3 +- tests/conftest.py | 49 +- tests/test_autodetect_memory.py | 194 ++++++- tests/test_brillouin_zone.py | 6 +- tests/test_bubble_gen.py | 34 +- ..._rank_0.npy => chi_phys_q_dens_rank_0.npy} | Bin ..._rank_0.npy => chi_phys_q_magn_rank_0.npy} | Bin .../end_2_end/vrg_q_dens_right_rank_0.npy | Bin 0 -> 645248 bytes .../end_2_end/vrg_q_magn_right_rank_0.npy | Bin 0 -> 645248 bytes tests/test_eliashberg_end_to_end.py | 133 ++++- tests/test_eliashberg_solver.py | 478 +++++++++++++++++- tests/test_four_point.py | 215 +++++++- tests/test_full_vertex_integration.py | 142 ++++++ tests/test_greens_function.py | 4 +- tests/test_hamiltonian.py | 2 +- tests/test_interaction.py | 56 +- tests/test_lambda_correction.py | 2 +- tests/test_local_four_point.py | 288 ++++++++--- tests/test_local_n_point.py | 36 +- tests/test_local_sde.py | 2 +- tests/test_local_sde_end_to_end.py | 4 +- tests/test_local_two_point.py | 2 +- tests/test_max_ent.py | 67 ++- tests/test_memory_estimator.py | 269 ++++++---- tests/test_mixing.py | 30 +- tests/test_mpi_utils.py | 96 +++- tests/test_n_point_base.py | 9 +- tests/test_nonlocal_sde.py | 172 ++++++- tests/test_nonlocal_sde_end_to_end.py | 4 +- tests/test_self_energy.py | 14 +- tests/test_symmetrize.py | 2 +- tests/test_symmetry_reduction.py | 26 +- 79 files changed, 3452 insertions(+), 914 deletions(-) create mode 100644 .github/workflows/Docs.yml create mode 100644 .github/workflows/Lint.yml rename tests/test_data/end_2_end/{gchi_aux_q_dens_sum_rank_0.npy => chi_phys_q_dens_rank_0.npy} (100%) rename tests/test_data/end_2_end/{gchi_aux_q_magn_sum_rank_0.npy => chi_phys_q_magn_rank_0.npy} (100%) create mode 100644 tests/test_data/end_2_end/vrg_q_dens_right_rank_0.npy create mode 100644 tests/test_data/end_2_end/vrg_q_magn_right_rank_0.npy create mode 100644 tests/test_full_vertex_integration.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d60f0707..828d8797 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,3 +5,7 @@ updates: directory: "/" # Location of package manifests schedule: interval: "monthly" + - package-ecosystem: "pip" + directory: "/" # root requirements.txt (docs/requirements.txt is intentionally pinned, left out) + schedule: + interval: "monthly" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ac4a887e..2b71a259 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,6 +10,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: name: Test on ${{ matrix.os }} / Python ${{ matrix.python-version }} @@ -57,7 +61,7 @@ jobs: # 7. Run tests with coverage - name: Run tests run: | - pytest tests --runslow --cov=dgamore --cov-report=term-missing --cov-report=xml -vv + pytest tests --runslow --cov=dgamore --cov-report=term-missing --cov-report=xml --cov-fail-under=85 -vv # pytest tests --runslow --cov=dgamore --cov-report=xml --cov-report=term # 8. Upload coverage to Codecov (requires CODECOV_TOKEN for private repos) diff --git a/.github/workflows/Docs.yml b/.github/workflows/Docs.yml new file mode 100644 index 00000000..06c4eb7f --- /dev/null +++ b/.github/workflows/Docs.yml @@ -0,0 +1,42 @@ +name: Docs + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build docs (Sphinx) + runs-on: ubuntu-latest + steps: + # 1. Checkout repository + - name: Checkout code + uses: actions/checkout@v6 + + # 2. Set up Python (mirrors the Read the Docs build: Python 3.13) + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + cache: 'pip' + + # 3. Install documentation dependencies (mpi4py and cupy are mocked in conf.py) + - name: Install documentation dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + + # 4. Build HTML docs; -W turns warnings into errors, --keep-going reports them all + - name: Build HTML documentation + run: | + sphinx-build -W --keep-going -b html docs docs/_build/html diff --git a/.github/workflows/Lint.yml b/.github/workflows/Lint.yml new file mode 100644 index 00000000..54eda63b --- /dev/null +++ b/.github/workflows/Lint.yml @@ -0,0 +1,38 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + black: + name: Black format check + runs-on: ubuntu-latest + steps: + # 1. Checkout repository + - name: Checkout code + uses: actions/checkout@v6 + + # 2. Set up Python + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + # 3. Install Black (pinned; keep local Black matching this version) + - name: Install Black + run: pip install black==26.5.1 + + # 4. Verify formatting (line-length also read from [tool.black] in pyproject.toml) + - name: Check formatting + run: black --check --line-length 120 . diff --git a/.typos.toml b/.typos.toml index cf67040f..b411adc9 100644 --- a/.typos.toml +++ b/.typos.toml @@ -6,6 +6,9 @@ extend-ignore-words-re = [ "iy", "iz", "ket", + "bse", + "BSE", + "gam", # numpy exposes the array flag as ``arr.flags.writeable``; not a typo. "writeable" ] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f6ad8ea..e056aa73 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,16 +69,18 @@ When you would like to contribute code, the following workflow keeps things smoo ```bash pytest tests # fast suite (skips tests marked slow) pytest tests --runslow # full suite, as run in CI - pytest tests --runslow --cov=dgamore --cov-report=term-missing # with coverage + pytest tests --runslow --cov=dgamore --cov-report=term-missing --cov-fail-under=85 # coverage, as CI runs it ``` 5. **Open a pull request** against the `main` branch, with a short description of what you changed and why. If your pull request is related to an existing issue, mentioning it helps connect the two. -A continuous integration pipeline runs the full test suite on every pull request, across Python 3.12 to 3.14 on both -Linux and macOS. This is there to catch regressions, not to be a gatekeeper, so please do not worry if something turns -red on the first try; it is a normal part of the process, and we are glad to help you get it passing. A coverage tool -also checks that the overall test coverage stays above 85%, so adding tests for your changes is the best way to keep it -healthy. +A continuous integration pipeline runs on every pull request. It checks that the code is Black-formatted, then runs the +full test suite across Python 3.12 to 3.14 on both Linux and macOS. This is there to catch regressions, not to be a +gatekeeper, so please do not worry if something turns red on the first try; it is a normal part of the process, and we +are glad to help you get it passing. The pipeline also requires the overall test coverage to stay at **at least 85%**, +and the build fails if it drops below that threshold. Beyond the overall figure, the new or changed code in a pull +request (the *patch*) must itself be covered to **at least 85%**, so please add tests for what you write rather than +relying on the rest of the code base to carry the average. ## Coding style diff --git a/README.md b/README.md index 93ae57be..1b805177 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Configure a run by editing your configuration file, then execute the routine wit holding it and `-c` to name it (defaults: the repository directory and [dga_config.yaml](dgamore/dga_config.yaml)): ```bash -mpiexec -np 8 DGAmore.py -p /configs/ -c my_config.yaml # or: DGAmore.py for a single-core test run +mpiexec -np 8 DGAmore -p /configs/ -c my_config.yaml # or: DGAmore for a single-core test run ``` See the [installation](https://dgamore.readthedocs.io/en/latest/installation.html) and diff --git a/codecov.yml b/codecov.yml index 0b6f1c3b..171d14be 100644 --- a/codecov.yml +++ b/codecov.yml @@ -26,7 +26,7 @@ coverage: default: target: 85% threshold: 0% - informational: true + informational: false comment: layout: "reach, diff, flags, files" behavior: default diff --git a/dgamore/DGAmore.py b/dgamore/DGAmore.py index 5e31a821..3d6060e7 100644 --- a/dgamore/DGAmore.py +++ b/dgamore/DGAmore.py @@ -2,11 +2,11 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ -Main entry point and top-level driver of a DGAmore run (installed on the PATH as ``DGAmore.py``). The -:func:`execute_dga_routine` orchestrates the full pipeline: parse the config, load the DMFT input, run the local +Main entry point and top-level driver of a DGAmore run (installed on the PATH as ``DGAmore``). The +:func:`main` orchestrates the full pipeline: parse the config, load the DMFT input, run the local Schwinger-Dyson step per inequivalent atom and assemble the full multi-band quantities, run the non-local ladder DGA self-energy, optionally analytically continue to real frequencies, and optionally solve the Eliashberg equation -- saving and plotting results along the way. Rank 0 owns the file I/O, local assembly and plotting; the @@ -16,7 +16,11 @@ import itertools as it import logging import os -from copy import deepcopy + +# OpenMPI: exclude the UCX one-sided (RMA) component before MPI is initialised. On some OpenMPI 5.x builds it fails its +# own component-query and prints a benign "OSC UCX component priority set inside component query failed" warning when +# the per-node shared-memory giwk window is created. +os.environ.setdefault("OMPI_MCA_osc", "^ucx") import matplotlib.pyplot as plt import numpy as np @@ -42,7 +46,7 @@ logging.getLogger("matplotlib").setLevel(logging.WARNING) -def execute_dga_routine(): +def main(): """ Runs the complete DGA pipeline end to end: config parsing and folder setup, DMFT input loading, the local Schwinger-Dyson step (per inequivalent atom, assembled into full multi-band quantities), the non-local @@ -224,7 +228,7 @@ def write_to_full_4pt_quantity(obj_full, obj_ineq: LocalFourPoint, sl: slice): :return: The full object with this atom's block filled in. """ if obj_full is None: - obj_full = deepcopy(obj_ineq) + obj_full = obj_ineq.copy() obj_full.mat = np.zeros( (config.sys.n_bands,) * 4 + obj_ineq.current_shape[4:], dtype=obj_ineq.mat.dtype ) @@ -246,7 +250,7 @@ def write_to_full_2pt_quantity( :return: The full object with this atom's block filled in. """ if obj_full is None: - obj_full = deepcopy(obj_ineq) + obj_full = obj_ineq.copy() obj_full.mat = np.zeros( ((1, 1, 1) + (config.sys.n_bands,) * 2 if has_momentum else (config.sys.n_bands,) * 2) + (obj_ineq.current_shape[-1],), @@ -530,25 +534,28 @@ def write_smom(obj_full: SelfEnergy, obj_ineq: SelfEnergy, sl: slice): def autodetect_memory_settings(comm: MPI.Comm) -> None: """ - Sets the five ``config.memory.save_memory_*`` switches automatically from the host memory available on every node - the job runs on and an analytic estimate of the peak memory each affected operation consumes. Must be called only - after the irreducible BZ is known (i.e. after auto-symmetry discovery), as the estimate depends on - ``q_grid.nk_irr``. + Sets the four ``config.memory.save_memory_*`` switches automatically from the host memory available on every node + the job runs on and an analytic estimate of the peak memory each affected operation consumes; the flag-less + Schwinger-Dyson contraction (always the two-pass FFT path) is verified to fit as well. Must be called only after + the irreducible BZ is known (i.e. after auto-symmetry discovery), as the estimate depends on ``q_grid.nk_irr``. The budget is a **node total**: on a node with ``r`` ranks the memory held by all of them at a branch's peak is - ``r * (baseline + distributed) + single`` (every rank holds the persistent baseline; a *distributed* transient is - held by every rank at once, a *single-rank* transient by one rank while the others idle), and this must not exceed - ``psutil.virtual_memory().available * 0.9`` for that node. Each node's rank count and available memory are - collected with a single ``allgather`` of ``(hostname, available_bytes)``; a branch's fast path is judged to "fit" - only if it fits on **every** node (the flags are process-wide, so the tightest node governs, and a single-rank - transient may land on any node). For each branch the fast path is checked and the flag is switched on if it would - not fit -- but an explicit ``True`` from the config is always kept (floor semantics: - ``final = user_flag or autodetect_on``). If even the lean path of any considered branch does not fit on some - node, a :class:`MemoryError` is raised. + ``r * (baseline + distributed) + single`` (every rank holds the branch's persistent baseline; a *distributed* + transient is held by every rank at once, a *single-rank* transient by one rank while the others idle), minus + ``(r - 1) * giwk_shareable`` when ``config.memory.use_shared_memory_giwk`` deduplicates the branch's ``giwk_full`` + to one copy per node, and this must not exceed ``psutil.virtual_memory().available * 0.9`` for that node. Each + node's rank count and available memory are collected with a single ``allgather`` of + ``(hostname, available_bytes)``; a branch's path is judged to "fit" only if it fits on **every** node (the flags + are process-wide, so the tightest node governs, and a single-rank transient may land on any node). The + ``lanczos`` fast-path single-rank peak is doubled on a single-node multi-rank job because the singlet and triplet + solves then run concurrently on the same node. For each branch the fast path is checked and the flag is switched + on if it would not fit -- but an explicit ``True`` from the config is always kept (floor semantics: + ``final = user_flag or autodetect_on``). A :class:`MemoryError` is raised only if the path that would actually + run does not fit. :param comm: The MPI communicator (used to group ranks by node). :return: None. - :raises MemoryError: If the most memory-lean path of any considered branch still overflows some node's budget. + :raises MemoryError: If the code path selected for some branch overflows some node's budget. """ logger = config.logger @@ -564,9 +571,9 @@ def autodetect_memory_settings(comm: MPI.Comm) -> None: nodes[host][1] = min(nodes[host][1], avail) niv_pp = min(config.box.niw_core // 2, config.box.niv_core // 2) - # Must mirror the giwk_full window kept through the SDE in nonlocal_sde.calculate_self_energy_q. + # Must mirror the giwk_full window the SDE section starts from in nonlocal_sde.calculate_self_energy_q. niv_cut = min(config.box.niw_core + config.box.niv_full + 10, config.box.niv_dmft) - baseline, peaks = memory_estimator.estimate_peaks( + peaks = memory_estimator.estimate_peaks( n_bands=config.sys.n_bands, nk_tot=config.lattice.q_grid.nk_tot, nk_irr=config.lattice.q_grid.nk_irr, @@ -579,54 +586,84 @@ def autodetect_memory_settings(comm: MPI.Comm) -> None: with_eliashberg=config.eliashberg.perform_eliashberg, save_fq=config.eliashberg.save_fq, construct_fq_cheap=config.eliashberg.construct_fq_cheap, + save_pairing_vertex=config.eliashberg.save_pairing_vertex, + n_eig=config.eliashberg.n_eig, ) - def node_total(distributed: float, single: float, n_ranks: int) -> float: - """Memory held on a node with ``n_ranks`` ranks at a branch's peak (see :func:`autodetect_memory_settings`).""" - return n_ranks * (baseline + distributed) + single + # The singlet and triplet in-memory Eliashberg solves run concurrently on two ranks; on a single-node multi-rank + # job both land on the same node, so its lanczos fast-path single-rank peak is doubled. + single_node_multi_rank = len(nodes) == 1 and comm.size >= 2 + + def node_total(bp: memory_estimator.BranchPeak, distributed: float, single: float, n_ranks: int) -> float: + """Memory held on a node with ``n_ranks`` ranks at a branch's peak (see :func:`autodetect_memory_settings`). + When ``config.memory.use_shared_memory_giwk`` is on, the branch's shareable ``giwk_full`` is counted once per + node instead of once per rank.""" + total = n_ranks * (bp.baseline + distributed) + single + if config.memory.use_shared_memory_giwk: + total -= (n_ranks - 1) * bp.giwk_shareable + return total - def fits_everywhere(distributed: float, single: float) -> bool: + def fits_everywhere(bp: memory_estimator.BranchPeak, distributed: float, single: float) -> bool: """Whether a transient (per-rank ``distributed`` + one-off ``single``) fits the 90% budget on every node.""" - return all(node_total(distributed, single, r) <= avail * 0.9 for r, avail in nodes.values()) + return all(node_total(bp, distributed, single, r) <= avail * 0.9 for r, avail in nodes.values()) flag_to_key = { "save_memory_for_chi0q": "chi0q", "save_memory_for_chiq_aux": "chiq_aux", - "save_memory_for_sde": "sde", "save_memory_for_fq": "fq", "save_memory_for_lanczos": "lanczos", } key_to_label = { "chi0q": "Bare bubble", "chiq_aux": "Auxiliary susceptibility", - "sde": "Schwinger-Dyson equation", "fq": "Full vertex", "lanczos": "Eliashberg solver", } - logger.info( - f"Auto memory detection (node-total budget): {len(nodes)} node(s), " - f"per-rank baseline {baseline / 1024**3:.3f} GB." - ) + logger.info(f"Auto memory detection (node-total budget): {len(nodes)} node(s).") + + # The Schwinger-Dyson contraction has no save_memory switch (the q-loop variant is unused - it peaked HIGHER + # than the two-pass FFT path); its single path is still checked so an oversized box fails fast, not mid-run. + if "sde" in peaks: + bp_sde = peaks["sde"] + if not fits_everywhere(bp_sde, bp_sde.off_distributed, bp_sde.off_single): + worst = max(node_total(bp_sde, bp_sde.off_distributed, bp_sde.off_single, r) for r, _ in nodes.values()) + raise MemoryError( + f"The Schwinger-Dyson equation needs {worst / 1024**3:.3f} GB on a node, which exceeds 90% of that " + f"node's available memory. Use more nodes, fewer ranks per node, a smaller frequency box or k-grid." + ) + worst_sde = max(node_total(bp_sde, bp_sde.off_distributed, bp_sde.off_single, r) for r, _ in nodes.values()) + logger.info( + f"Schwinger-Dyson equation: per-rank baseline {bp_sde.baseline / 1024**3:.3f} GB, " + f"node total {worst_sde / 1024**3:.3f} GB (single FFT path, no memory-saving switch)." + ) for attr, key in flag_to_key.items(): if key not in peaks: continue bp = peaks[key] label = key_to_label[key] - if not fits_everywhere(bp.on_distributed, bp.on_single): - worst = max(node_total(bp.on_distributed, bp.on_single, r) for r, _ in nodes.values()) + off_single = bp.off_single * (2 if key == "lanczos" and single_node_multi_rank else 1) + fits_off = fits_everywhere(bp, bp.off_distributed, off_single) + fits_on = fits_everywhere(bp, bp.on_distributed, bp.on_single) + autodetect_on = not fits_off + final = bool(getattr(config.memory, attr)) or autodetect_on + if final and not fits_on: + worst = max(node_total(bp, bp.on_distributed, bp.on_single, r) for r, _ in nodes.values()) raise MemoryError( - f"Even the most memory-lean path for '{label}' needs {worst / 1024**3:.3f} GB on a node, which " - f"exceeds 90% of that node's available memory. Use more nodes, fewer ranks per node, a smaller " - f"frequency box or k-grid." + f"The memory-saving path for '{label}' needs {worst / 1024**3:.3f} GB on a node, which exceeds 90% " + f"of that node's available memory" + + ( + " (and its fast path does not fit either)" + if autodetect_on + else " (its fast path would fit; unset the save_memory flag)" + ) + + ". Use more nodes, fewer ranks per node, a smaller frequency box or k-grid." ) - autodetect_on = not fits_everywhere(bp.off_distributed, bp.off_single) - final = bool(getattr(config.memory, attr)) or autodetect_on setattr(config.memory, attr, final) - worst_off = max(node_total(bp.off_distributed, bp.off_single, r) for r, _ in nodes.values()) + worst_off = max(node_total(bp, bp.off_distributed, off_single, r) for r, _ in nodes.values()) logger.info( - f"{label}: fast-path node total {worst_off / 1024**3:.3f} GB -> " - f"memory saving {'enabled' if final else 'disabled'}." + f"{label}: per-rank baseline {bp.baseline / 1024**3:.3f} GB, fast-path node total " + f"{worst_off / 1024**3:.3f} GB -> memory saving {'enabled' if final else 'disabled'}." ) @@ -702,4 +739,4 @@ def configure_matplotlib(): if __name__ == "__main__": - execute_dga_routine() + main() diff --git a/dgamore/__init__.py b/dgamore/__init__.py index 09901cb1..5c1a04f0 100644 --- a/dgamore/__init__.py +++ b/dgamore/__init__.py @@ -1,6 +1,5 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems - diff --git a/dgamore/brillouin_zone.py b/dgamore/brillouin_zone.py index 75f7bcec..b2a45925 100644 --- a/dgamore/brillouin_zone.py +++ b/dgamore/brillouin_zone.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ @@ -361,7 +361,7 @@ def get_lattice_symmetries_from_string(symmetry_string: str | tuple | list) -> l # Sentinel object returned by get_lattice_symmetries_from_string for "auto". -# Identity-checked, so it must be a unique singleton — a small dedicated object. +# Identity-checked, so it must be a unique singleton - a small dedicated object. class _AutoSymmetriesSentinel: """Marker indicating that lattice symmetries are to be detected automatically from a Hamiltonian, via KGrid.specify_auto_symmetries().""" diff --git a/dgamore/bubble_gen.py b/dgamore/bubble_gen.py index cbec3614..45b8e06a 100644 --- a/dgamore/bubble_gen.py +++ b/dgamore/bubble_gen.py @@ -1,11 +1,11 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Generalized bare susceptibilities (the "bubbles"). :class:`BubbleGenerator` builds the products of two Green's -functions :math:`\chi_{0;abcd} = -\beta\, G_{ad}\, G_{cb}` in the particle-hole and particle-particle channels, +functions :math:`\chi_{0;1234}^{\omega\nu} = -\beta\, G_{14}^{\nu}\, G_{32}^{\nu-\omega}` in the particle-hole and particle-particle channels, both local and momentum-dependent. The non-local versions are evaluated either by an FFT over the BZ or by a direct momentum-shift einsum, distributed over MPI ranks and optionally accelerated on the GPU (CuPy). """ @@ -32,7 +32,7 @@ class BubbleGenerator: def create_generalized_chi0(g_dmft: GreensFunction, niw: int, niv: int, beta: float) -> LocalFourPoint: r""" Returns the local generalized bare susceptibility - :math:`\chi_{0;abcd}^{\omega\nu} = -\beta\, G_{ad}^{\nu}\, G_{cb}^{\nu-\omega}`. + :math:`\chi_{0;1234}^{\omega\nu} = -\beta\, G_{14}^{\nu}\, G_{32}^{\nu-\omega}`. :param g_dmft: The local (DMFT) :class:`GreensFunction`. :param niw: Number of positive bosonic frequencies. @@ -59,7 +59,7 @@ def create_generalized_chi0_q_fft( ) -> FourPoint: r""" Returns the momentum-dependent generalized bare susceptibility - :math:`\chi_{0;abcd}^{q\nu} = -\beta \sum_k G^{k}_{ad}\, G^{k-q}_{cb}`, evaluated via an FFT over the BZ with + :math:`\chi_{0;1234}^{q\omega\nu} = -\beta \sum_k G^{k\nu}_{14}\, G^{(k-q)(\nu-\omega)}_{32}`, evaluated via an FFT over the BZ with preallocated buffers. The result is computed on rank 0 over the irreducible BZ and scattered across ranks. :param mpi_dist_irrk: MPI distributor over the irreducible BZ q-points (see :class:`MpiDistributor`). @@ -173,7 +173,7 @@ def create_generalized_chi0_q( ) -> FourPoint: r""" Returns the momentum-dependent generalized bare susceptibility - :math:`\chi_{0;abcd}^{q\nu} = -\beta \sum_k G^{k}_{ad}\, G^{k-q}_{cb}`, evaluated by a direct momentum-shift + :math:`\chi_{0;1234}^{q\omega\nu} = -\beta \sum_k G^{k\nu}_{14}\, G^{(k-q)(\nu-\omega)}_{32}`, evaluated by a direct momentum-shift and a fused einsum over the explicit list of q-points (preallocated buffers). :param giwk: The momentum-dependent :class:`GreensFunction`. @@ -279,7 +279,7 @@ def create_generalized_chi0_q_auto( def create_generalized_chi0_pp_w0(g_dmft: GreensFunction, niv_pp: int, beta: float) -> LocalFourPoint: r""" Returns the local particle-particle bare bubble at :math:`\omega = 0`, - :math:`\chi_{0;abcd}^{\nu} = -\beta\, G_{ad}^{\nu}\, G_{cb}^{-\nu}`. + :math:`\chi_{0;1234}^{\nu} = -\beta\, G_{14}^{\nu}\, G_{32}^{-\nu}`. :param g_dmft: The local (DMFT) :class:`GreensFunction`. :param niv_pp: Number of positive fermionic frequencies of the pp bubble. @@ -301,7 +301,7 @@ def create_generalized_chi0_pp_w0(g_dmft: GreensFunction, niv_pp: int, beta: flo def create_generalized_chi0_q_pp_w0(giwk: GreensFunction, niv_pp: int, q_grid: KGrid) -> FourPoint: r""" Returns the momentum-dependent particle-particle bare bubble at :math:`\omega = 0`, - :math:`\chi_{0;abcd}^{\vec{k}(\omega=0)\nu} = G_{ad}^{k}\, G_{bc}^{-k}` with :math:`G_{bc}^{-k} = G_{cb}^{*k}`. + :math:`\chi_{0;1234}^{k(\omega=0)\nu} = G_{14}^{k\nu}\, G_{23}^{(-k)(-\nu)}` with :math:`G_{23}^{(-k)(-\nu)} = G_{32}^{*k\nu}`. Note that no factor of :math:`-\beta` is included here. :param giwk: The momentum-dependent :class:`GreensFunction`. diff --git a/dgamore/config.py b/dgamore/config.py index 8745d6f4..aa71bd5c 100644 --- a/dgamore/config.py +++ b/dgamore/config.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Global configuration singleton. This module holds the process-wide mutable state of a DGAmore run as module-level @@ -151,6 +151,9 @@ class EliashbergConfig: :ivar float epsilon: Convergence tolerance for the Lanczos eigensolver. :ivar str symmetry: Initial gap-function symmetry (``"d-wave"``, ``"p-wave-x"``, ``"p-wave-y"``, or ``"random"``). :ivar bool include_local_part: Whether to add the local reducible pp diagrams to the pairing vertex. + :ivar bool symmetrize_degenerate_gaps: Whether to orthonormalize the gap functions within (near-)degenerate + eigenvalue clusters and rotate doublets to the mirror-adapted basis (see + :func:`~dgamore.eliashberg_solver.symmetrize_degenerate_gaps`). :ivar str subfolder_name: Output subfolder name for Eliashberg results. """ @@ -163,6 +166,7 @@ def __init__(self): self.epsilon: float = 1e-6 self.symmetry: str = "random" self.include_local_part: bool = True + self.symmetrize_degenerate_gaps: bool = True self.subfolder_name: str = "Eliashberg" @@ -275,17 +279,19 @@ class MemoryConfig: :ivar bool save_memory_for_chi0q: Use the per-q einsum bubble instead of the FFT bubble. :ivar bool save_memory_for_chiq_aux: Use the per-q auxiliary-susceptibility path and per-rank BZ mapping. - :ivar bool save_memory_for_sde: Use the q-loop self-energy contraction instead of the FFT one. :ivar bool save_memory_for_fq: Use the per-q full-vertex construction in the Eliashberg step. :ivar bool save_memory_for_lanczos: Use the frequency-distributed Lanczos solver. + :ivar bool use_shared_memory_giwk: Store the replicated full-grid Green's function ``giwk_full`` in one MPI + shared-memory window per node (computed only by the node root) instead of one private copy per rank. Enabled + by default; disable it if cross-socket (NUMA) reads of the shared buffer outweigh the memory saving. """ def __init__(self): self.save_memory_for_chi0q: bool = False self.save_memory_for_chiq_aux: bool = False - self.save_memory_for_sde: bool = False self.save_memory_for_fq: bool = False self.save_memory_for_lanczos: bool = False + self.use_shared_memory_giwk: bool = True class AnaContConfig: diff --git a/dgamore/config_parser.py b/dgamore/config_parser.py index dc040ae1..75089ca3 100644 --- a/dgamore/config_parser.py +++ b/dgamore/config_parser.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ YAML configuration parsing. :class:`ConfigParser` reads the run's YAML config (rank 0), broadcasts it to all MPI @@ -260,6 +260,9 @@ def _build_eliashberg_config(self, config_file) -> EliashbergConfig: conf.epsilon = self._try_parse(section, "epsilon", conf.epsilon) conf.symmetry = self._try_parse(section, "symmetry", conf.symmetry) conf.include_local_part = self._try_parse(section, "include_local_part", conf.include_local_part) + conf.symmetrize_degenerate_gaps = self._try_parse( + section, "symmetrize_degenerate_gaps", conf.symmetrize_degenerate_gaps + ) conf.subfolder_name = self._try_parse(section, "subfolder_name", conf.subfolder_name) return conf @@ -323,9 +326,9 @@ def _build_memory_config(self, config_file): conf.save_memory_for_chiq_aux = self._try_parse( section, "save_memory_for_chiq_aux", conf.save_memory_for_chiq_aux ) - conf.save_memory_for_sde = self._try_parse(section, "save_memory_for_sde", conf.save_memory_for_sde) conf.save_memory_for_fq = self._try_parse(section, "save_memory_for_fq", conf.save_memory_for_fq) conf.save_memory_for_lanczos = self._try_parse(section, "save_memory_for_lanczos", conf.save_memory_for_lanczos) + conf.use_shared_memory_giwk = self._try_parse(section, "use_shared_memory_giwk", conf.use_shared_memory_giwk) return conf diff --git a/dgamore/dga_config.yaml b/dgamore/dga_config.yaml index 6f443665..1eff2058 100644 --- a/dgamore/dga_config.yaml +++ b/dgamore/dga_config.yaml @@ -48,15 +48,15 @@ dmft_input: # for multi-orbital systems with more than one inequivalent atom specified in w2dynamics. eliashberg: - perform_eliashberg: False # If True, the code will perform a power iteration to solve the linearized Eliashberg equation for the superconducting eigenvalues and gap functions + perform_eliashberg: False # If True, the code will perform an ARNOLDI Lanczos to solve the linearized Eliashberg equation for the superconducting eigenvalues and gap functions save_pairing_vertex: False # if true, saves the s/t pairing vertices Gamma(qvv') for the irreducible BZ to files. Attention: this could be a large file! save_fq: False # If True, saves the full d/m vertex F(qvv') for the irreducible BZ to files. Attention: this could be a large file! construct_fq_cheap: False # If True, the code will construct F(qvv') for the d/m channels in a way that is less memory intensive (only 1/4) but also less accurate. This is recommended for large frequency boxes. n_eig: 4 # Number of eigenvalues and eigenvectors to be calculated. Default: 4 epsilon: 1e-6 # Convergence criterion for the eigenvalues and eigenvectors - symmetry: "random" # Symmetry of the gap function that will be used as a starting point for the power iteration. Available: p-wave-x, p-wave-y, d-wave, random + symmetry: "random" # Symmetry of the gap function that will be used as a starting point for the ARNOLDI Lanczos. Available: p-wave-x, p-wave-y, d-wave, random include_local_part: True # If True, the local part of the pairing vertex is included in the Eliashberg calculation. This is only relevant if one might expect s-wave symmetry. - # setting this to True will reduce the frequency box of the pairing vertex slightly due to an additional frequency shift that is necessary. + symmetrize_degenerate_gaps: True # If True, gap functions within (near-)degenerate eigenvalue clusters are orthogonalized (Loewdin) and doublets rotated to the mirror-adapted p_x/p_y basis. subfolder_name: "Eliashberg" # name for the subfolder where certain outputs are generated self_energy_interpolation: @@ -89,11 +89,10 @@ memory: # but much slower. If False, the code will use FFT, which requires larger intermediate arrays, but is much faster. save_memory_for_chiq_aux: False # If True, the code will construct the auxiliary susceptibility chi(q) (equal in size # compared to F(q)) in a way that is more memory efficient, but much slower. - save_memory_for_sde: False # If True, the code will perform the calculations in a more - # memory efficient way, without the use of FFTs. Setting it to False will not increase the memory usage significantly, - # but it is much faster. save_memory_for_fq: False # If True, the code will construct F(qvv') for the d/m channels in a way that is # less memory intensive, but it will increase runtime. save_memory_for_lanczos: False # If True, the code will perform the Lanczos algorithm with the pairing vertex for # the Eliashberg calculation in a more memory efficient way. This will reduce memory usage but increase runtime - # due to required MPI communication in each Lanczos step. \ No newline at end of file + # due to required MPI communication in each Lanczos step. + use_shared_memory_giwk: True # If True (default), the full-grid lattice Green's function is stored once per node in + # an MPI shared-memory window (computed only by the node's first rank) \ No newline at end of file diff --git a/dgamore/dga_io.py b/dgamore/dga_io.py index a8561279..d57e21ed 100644 --- a/dgamore/dga_io.py +++ b/dgamore/dga_io.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ High-level DMFT input loading and run setup. This module ties the DMFT interface, the global config and the lattice diff --git a/dgamore/dga_logger.py b/dgamore/dga_logger.py index 5cc6036e..6146ce9a 100644 --- a/dgamore/dga_logger.py +++ b/dgamore/dga_logger.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ MPI-aware logging. :class:`DgaLogger` timestamps messages and, by default, only emits them on the root rank so diff --git a/dgamore/dmft_interface.py b/dgamore/dmft_interface.py index 06162121..3f766ca3 100644 --- a/dgamore/dmft_interface.py +++ b/dgamore/dmft_interface.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ DMFT input interface. :class:`DMFTInterface` is the abstract contract for reading the quantities a DGA run needs diff --git a/dgamore/eliashberg_solver.py b/dgamore/eliashberg_solver.py index 32c22cfd..0c516e51 100644 --- a/dgamore/eliashberg_solver.py +++ b/dgamore/eliashberg_solver.py @@ -1,14 +1,14 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Linearized Eliashberg equation solver. Starting from the ladder-DGA full vertex (saved per channel by the non-local SDE step), this module assembles the particle-particle pairing vertex in the singlet/triplet channels at -:math:`\omega = 0`, optionally adds the local reducible diagrams, and power-iterates the linearized gap equation -:math:`\lambda \Delta = \pm\frac{1}{2\beta N_q}\, \Gamma^{pp}\, \chi_0^{pp}\, \Delta` via an ARPACK/Lanczos -eigensolver (two variants: an in-memory one and a memory-lean frequency-distributed one). The leading +:math:`\omega = 0`, optionally adds the local reducible diagrams, and solves the linearized gap equation +:math:`\lambda \Delta = \pm\frac{1}{2\beta N_q}\, \Gamma^{pp}\, \chi_0^{pp}\, \Delta` with a matrix-free +ARPACK/Lanczos eigensolver (two variants: an in-memory one and a memory-lean frequency-distributed one). The leading eigenvalue :math:`\lambda` signals the pairing instability and the eigenvector is the gap function :math:`\Delta(k, \nu)`. Requires ``nq == nk``. Equation numbers refer to the author's master's thesis (Chapter 4). """ @@ -57,8 +57,19 @@ def delete_files(filepath: str, *args) -> None: def _transform_vertex_frequencies_w0(vertex: LocalFourPoint | FourPoint, niv_pp: int) -> np.ndarray: r""" Transforms a vertex from particle-hole to particle-particle notation at :math:`\omega' = 0`, following Motoharu - Kitatani's frequency convention: the fermionic frequency is flipped and the bosonic index is remapped via - :math:`\omega = \nu - \nu'`. + Kitatani's frequency convention: the fermionic frequency is flipped, the bosonic index is remapped via + :math:`\omega = \nu - \nu'` and the orbitals are permuted to :math:`1432`. In full index notation the output is + + .. math:: \bar{F}^{pp;\nu\nu'}_{1234} = -F^{ph;\,\omega=\nu-\nu';\ \nu_1=\nu,\ \nu_2=-\nu'}_{1432} + = -F^{ph;(\nu-\nu')\nu(-\nu')}_{1432}, + + i.e. (minus) the crossed-slot form of the pairing vertex of Eq. (4.49) in my thesis: with the ph frequency + convention of Eq. (3.28a) the four legs of :math:`\bar{F}^{pp;\nu\nu'}_{1234}` carry the frequencies + :math:`(\nu, \nu', -\nu, -\nu')` on the orbitals :math:`(1, 4, 3, 2)`. The overall minus is the sign of the + power-iteration matrix :math:`M = -\Gamma\chi` of Eq. (4.42). Used by + :func:`transform_vertex_loc_frequencies_w0` and :func:`transform_vertex_q_frequencies_w0`; the direct-slot + counterpart (:math:`\omega_{ph} = \nu + \nu'`, no flip, orbitals :math:`1234`) is + :meth:`~dgamore.local_four_point.LocalFourPoint.change_frequency_notation_ph_to_pp_w0`. :param vertex: The vertex to transform (:class:`LocalFourPoint` or :class:`FourPoint`) in ph notation. :param niv_pp: Number of positive fermionic frequencies of the pp vertex. @@ -68,7 +79,12 @@ def _transform_vertex_frequencies_w0(vertex: LocalFourPoint | FourPoint, niv_pp: wn = MFHelper.wn(config.box.niw_core) omega = vn[:, None] - vn[None, :] - vertex = vertex.cut_niv(niv_pp).to_full_niw_range().flip_frequency_axis(-1, False) + vertex = ( + vertex.cut_niv(niv_pp) + .to_full_niw_range() + .permute_orbitals("abcd->adcb", copy=False) + .flip_frequency_axis(-1, False) + ) f_q_r_pp_mat = np.zeros((*vertex.current_shape[:-3], 2 * niv_pp, 2 * niv_pp), dtype=vertex.mat.dtype) for idx, w in enumerate(wn): @@ -133,7 +149,7 @@ def create_full_vertex_q_r( f_q_r = nonlocal_sde.create_auxiliary_chi_r_q(gamma_r, gchi0_q_inv, u_loc, v_nonloc) logger.info(f"Non-Local auxiliary susceptibility ({gamma_r.channel.value}) calculated.") - f_q_r = config.sys.beta**2 * (gchi0_q_inv - gchi0_q_inv @ f_q_r @ gchi0_q_inv) + f_q_r = (gchi0_q_inv - gchi0_q_inv @ f_q_r @ gchi0_q_inv).scale(config.sys.beta**2) gchi0_q_inv.free() if not config.eliashberg.save_fq: @@ -143,28 +159,38 @@ def create_full_vertex_q_r( logger.info(f"Calculated first part of full {gamma_r.channel.value} vertex.") - vrg_q_r = FourPoint.load( + vrg_q_r_left = FourPoint.load( os.path.join(config.output.eliashberg_path, f"vrg_q_{gamma_r.channel.value}_rank_{mpi_dist.my_rank}.npy"), channel=gamma_r.channel, num_vn_dimensions=1, ) if config.eliashberg.construct_fq_cheap: - vrg_q_r = vrg_q_r.cut_niv(niv_pp) + vrg_q_r_left = vrg_q_r_left.cut_niv(niv_pp) - gchi_aux_q_r_sum = FourPoint.load( - os.path.join( - config.output.eliashberg_path, f"gchi_aux_q_{gamma_r.channel.value}_sum_rank_{mpi_dist.my_rank}.npy" - ), + chi_phys_q_r = FourPoint.load( + os.path.join(config.output.eliashberg_path, f"chi_phys_q_{gamma_r.channel.value}_rank_{mpi_dist.my_rank}.npy"), channel=gamma_r.channel, num_vn_dimensions=0, ) - logger.info(f"Loaded vrg_q_{gamma_r.channel.value} and gchi_aux_q_{gamma_r.channel.value}_sum from files.") + logger.info(f"Loaded vrg_q_{gamma_r.channel.value} and chi_phys_q_{gamma_r.channel.value} from files.") u = u_loc.as_channel(gamma_r.channel) + v_nonloc.as_channel(gamma_r.channel) - f_q_r_2 = u @ (vrg_q_r * vrg_q_r) - u @ gchi_aux_q_r_sum @ u @ (vrg_q_r * vrg_q_r) - vrg_q_r.free() - gchi_aux_q_r_sum.free() + f_q_r_2 = vrg_q_r_left @ u - vrg_q_r_left @ (u @ chi_phys_q_r @ u) + vrg_q_r_left.free() + chi_phys_q_r.free() + + vrg_q_r_right = FourPoint.load( + os.path.join(config.output.eliashberg_path, f"vrg_q_{gamma_r.channel.value}_right_rank_{mpi_dist.my_rank}.npy"), + channel=gamma_r.channel, + num_vn_dimensions=1, + ) + + if config.eliashberg.construct_fq_cheap: + vrg_q_r_right = vrg_q_r_right.cut_niv(niv_pp) + + f_q_r_2 = f_q_r_2 * vrg_q_r_right + vrg_q_r_right.free() if not config.eliashberg.save_fq: f_q_r_2 = transform_vertex_q_frequencies_w0(f_q_r_2, niv_pp) @@ -178,7 +204,8 @@ def create_full_vertex_q_r( delete_files( config.output.eliashberg_path, f"vrg_q_{gamma_r.channel.value}_rank_{mpi_dist.my_rank}.npy", - f"gchi_aux_q_{gamma_r.channel.value}_sum_rank_{mpi_dist.my_rank}.npy", + f"vrg_q_{gamma_r.channel.value}_right_rank_{mpi_dist.my_rank}.npy", + f"chi_phys_q_{gamma_r.channel.value}_rank_{mpi_dist.my_rank}.npy", ) return f_q_r @@ -226,8 +253,9 @@ def create_full_vertex_q_r_v2( v_nonloc: Interaction, gamma_r: LocalFourPoint, gchi0_q_inv: FourPoint, - vrg_q_r: FourPoint, - gchi_aux_q_r_sum: FourPoint, + vrg_q_r_left: FourPoint, + vrg_q_r_right: FourPoint, + chi_phys_q_r: FourPoint, niv_pp: int, q_index: int, ) -> FourPoint: @@ -239,29 +267,30 @@ def create_full_vertex_q_r_v2( :param v_nonloc: The non-local interaction :math:`V^{q}`. :param gamma_r: The local irreducible vertex :math:`\Gamma_{r}` for this channel. :param gchi0_q_inv: The inverse bare bubble :math:`(\chi_0^q)^{-1}` over all rank-local q-points. - :param vrg_q_r: The momentum-dependent three-leg vertex :math:`\gamma^q_{r}`. - :param gchi_aux_q_r_sum: The summed auxiliary susceptibility :math:`\sum_{\nu'}\chi^{*;q}_{r}`. + :param vrg_q_r_left: The momentum-dependent three-leg vertex :math:`\gamma^q_{r}`. + :param vrg_q_r_right: The momentum-dependent "right-side" three-leg vertex :math:`\gamma^q_{r}`. + :param chi_phys_q_r: The physical susceptibility :math:`\chi^{phys;q}_{r}`. :param niv_pp: Number of positive fermionic frequencies of the pp vertex. :param q_index: Index of the q-point (into the rank-local list) to compute. :return: The full ladder vertex :math:`F^{q}_{r}` for that q-point as a :class:`FourPoint`. """ gchi0_q_inv_idx = gchi0_q_inv.filter_q_index(q_index) - vrg_q_r_idx = vrg_q_r.filter_q_index(q_index) - gchi_aux_q_r_sum_idx = gchi_aux_q_r_sum.filter_q_index(q_index) + vrg_q_r_left_idx = vrg_q_r_left.filter_q_index(q_index) + vrg_q_r_right_idx = vrg_q_r_right.filter_q_index(q_index) + chi_phys_q_r_idx = chi_phys_q_r.filter_q_index(q_index) v_nonloc_idx = v_nonloc.filter_q_index(q_index) u = u_loc.as_channel(gamma_r.channel) + v_nonloc_idx.as_channel(gamma_r.channel) f_q_r_idx = nonlocal_sde.create_auxiliary_chi_r_q(gamma_r, gchi0_q_inv_idx, u_loc, v_nonloc_idx) - f_q_r_idx = ( - config.sys.beta**2 * (gchi0_q_inv_idx - gchi0_q_inv_idx @ f_q_r_idx @ gchi0_q_inv_idx) - + u @ (vrg_q_r_idx * vrg_q_r_idx) - - u @ gchi_aux_q_r_sum_idx @ u @ (vrg_q_r_idx * vrg_q_r_idx) - ) + f_q_r_idx = (gchi0_q_inv_idx - gchi0_q_inv_idx @ f_q_r_idx @ gchi0_q_inv_idx).scale(config.sys.beta**2) + ( + vrg_q_r_left_idx @ u - vrg_q_r_left_idx @ (u @ chi_phys_q_r_idx @ u) + ) * vrg_q_r_right_idx gchi0_q_inv_idx.free() - vrg_q_r_idx.free() - gchi_aux_q_r_sum_idx.free() + vrg_q_r_left_idx.free() + vrg_q_r_right_idx.free() + chi_phys_q_r_idx.free() if not config.eliashberg.save_fq: f_q_r_idx = transform_vertex_q_frequencies_w0(f_q_r_idx, niv_pp) @@ -292,15 +321,23 @@ def create_full_vertex_q_r_pp_w0_v2( ) logger.info(f"Loaded gchi0_q_inv from file.") - vrg_q_r = FourPoint.load( + vrg_q_r_left = FourPoint.load( os.path.join(config.output.eliashberg_path, f"vrg_q_{gamma_r.channel.value}_rank_{mpi_dist_irrk.my_rank}.npy"), channel=gamma_r.channel, num_vn_dimensions=1, ) - gchi_aux_q_r_sum = FourPoint.load( + vrg_q_r_right = FourPoint.load( + os.path.join( + config.output.eliashberg_path, f"vrg_q_{gamma_r.channel.value}_right_rank_{mpi_dist_irrk.my_rank}.npy" + ), + channel=gamma_r.channel, + num_vn_dimensions=1, + ) + + chi_phys_q_r = FourPoint.load( os.path.join( - config.output.eliashberg_path, f"gchi_aux_q_{gamma_r.channel.value}_sum_rank_{mpi_dist_irrk.my_rank}.npy" + config.output.eliashberg_path, f"chi_phys_q_{gamma_r.channel.value}_rank_{mpi_dist_irrk.my_rank}.npy" ), channel=gamma_r.channel, num_vn_dimensions=0, @@ -309,9 +346,10 @@ def create_full_vertex_q_r_pp_w0_v2( if config.eliashberg.construct_fq_cheap: gamma_r = gamma_r.cut_niv(niv_pp) gchi0_q_inv = gchi0_q_inv.cut_niv(niv_pp) - vrg_q_r = vrg_q_r.cut_niv(niv_pp) + vrg_q_r_left = vrg_q_r_left.cut_niv(niv_pp) + vrg_q_r_right = vrg_q_r_right.cut_niv(niv_pp) - logger.info(f"Loaded vrg_q_{gamma_r.channel.value} and gchi_aux_q_{gamma_r.channel.value}_sum from files.") + logger.info(f"Loaded vrg_q_{gamma_r.channel.value} and chi_phys_q_{gamma_r.channel.value} from files.") irrk_q_list = config.lattice.q_grid.get_irrq_list() my_irr_q_list = irrk_q_list[mpi_dist_irrk.my_slice] @@ -335,19 +373,21 @@ def create_full_vertex_q_r_pp_w0_v2( for idx, q in enumerate(my_irr_q_list): f_q_r_mat[idx] = create_full_vertex_q_r_v2( - u_loc, v_nonloc, gamma_r, gchi0_q_inv, vrg_q_r, gchi_aux_q_r_sum, niv_pp, idx + u_loc, v_nonloc, gamma_r, gchi0_q_inv, vrg_q_r_left, vrg_q_r_right, chi_phys_q_r, niv_pp, idx ).mat logger.info(f"Full ladder-vertex ({gamma_r.channel.value}) calculated.") gchi0_q_inv.free() - vrg_q_r.free() - gchi_aux_q_r_sum.free() + vrg_q_r_left.free() + vrg_q_r_right.free() + chi_phys_q_r.free() delete_files( config.output.eliashberg_path, f"vrg_q_{gamma_r.channel.value}_rank_{mpi_dist_irrk.my_rank}.npy", - f"gchi_aux_q_{gamma_r.channel.value}_sum_rank_{mpi_dist_irrk.my_rank}.npy", + f"vrg_q_{gamma_r.channel.value}_right_rank_{mpi_dist_irrk.my_rank}.npy", + f"chi_phys_q_{gamma_r.channel.value}_rank_{mpi_dist_irrk.my_rank}.npy", ) if not config.eliashberg.save_fq: @@ -374,39 +414,180 @@ def create_full_vertex_q_r_pp_w0_v2( # --- Local particle-particle reducible diagrams (w=0) --- -def create_local_ud_diagrams_pp_w0(g_dmft: GreensFunction) -> tuple[LocalFourPoint, LocalFourPoint, LocalFourPoint]: +def create_local_gamma_ud_pp_w0( + gchi_ud_pp_w0: LocalFourPoint, gchi0_pp_w0: LocalFourPoint, beta: float +) -> LocalFourPoint: r""" - Builds the local particle-particle reducible diagrams at :math:`\omega = 0` in the up-down channel: the full - vertex :math:`F^{ud}`, the irreducible vertex :math:`\Gamma^{ud}`, and the reducible part - :math:`\Phi^{ud} = F^{ud} - \Gamma^{ud}`. These are the local diagrams subtracted/added when - ``include_local_part`` is enabled, to avoid double counting the local pairing contribution. + Returns the local pp-irreducible up-down vertex at :math:`\omega = 0` from the crossing-decoupled pp + Bethe-Salpeter equation, + + .. math:: \Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234} = \beta^2 \left[\chi^{pp}_0 J - \chi^{pp}_0\, + (\chi^{pp}_{\uparrow\downarrow})^{-1}\, \chi^{pp}_0\right]^{-1;\,\nu\nu'}_{1234}. + + All products and inverses live in compound pp index space, i.e. as matrices + :math:`M_{(13\nu),(42\nu')} = X^{pp;\nu\nu'}_{1234}` with the product and unit element + + .. math:: (X Y)^{pp;\nu\nu'}_{1234} = \sum_{ab\nu_1} X^{pp;\nu\nu_1}_{1a3b}\, Y^{pp;\nu_1\nu'}_{b2a4}, \qquad + \mathbb{1}^{pp;\nu\nu'}_{1234} = \delta_{14}\,\delta_{23}\,\delta_{\nu\nu'}. + + The ingredients in full index notation are the diagonal bare pp bubble, built from the local DMFT Green's + function :math:`G^{\mathrm{DMFT}}_{12}(\nu)`, and its image under the crossing operator :math:`J` + (:math:`\nu' \to -\nu'` combined with the orbital permutation :math:`1234 \to 1432`, i.e. + :math:`(XJ)^{pp;\nu\nu'}_{1234} = X^{pp;\nu(-\nu')}_{1432}`), + + .. math:: \chi^{pp;\nu\nu'}_{0;1234} = -\beta\, G^{\mathrm{DMFT}}_{14}(\nu)\, G^{\mathrm{DMFT}}_{32}(-\nu)\, + \delta_{\nu\nu'}, \qquad (\chi^{pp}_0 J)^{\nu\nu'}_{1234} = -\beta\, G^{\mathrm{DMFT}}_{12}(\nu)\, + G^{\mathrm{DMFT}}_{34}(-\nu)\, \delta_{\nu,-\nu'}. + + The returned :math:`\Gamma^{pp}_{\uparrow\downarrow}` is equivalent to solving the crossing-decoupled pp BSE + + .. math:: F^{pp;\nu\nu'}_{\uparrow\downarrow;1234} = \Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234} + - \frac{1}{\beta} \sum_{\nu_1} \sum_{abcd} \Gamma^{pp;\nu\nu_1}_{\uparrow\downarrow;1a3b}\, + G^{\mathrm{DMFT}}_{bc}(\nu_1)\, G^{\mathrm{DMFT}}_{ad}(-\nu_1)\, + F^{pp;(-\nu_1)\nu'}_{\uparrow\downarrow;d2c4} + + for the full vertex :math:`F^{pp}_{\uparrow\downarrow}` defined by amputating the DMFT legs of the + susceptibility, + + .. math:: \chi^{pp;\nu\nu'}_{\uparrow\downarrow;1234} = -\sum_{abcd} F^{pp;\nu\nu'}_{\uparrow\downarrow;abcd}\, + G^{\mathrm{DMFT}}_{1a}(\nu)\, G^{\mathrm{DMFT}}_{b2}(-\nu')\, G^{\mathrm{DMFT}}_{3c}(-\nu)\, + G^{\mathrm{DMFT}}_{d4}(\nu'). + + Note that :math:`\chi^{pp}_{\uparrow\downarrow}` must be the CONNECTED susceptibility: the disconnected + straight term :math:`\delta_{\omega_{ph} 0}\, \beta\, G^{\mathrm{DMFT}}_{12}(\nu)\, G^{\mathrm{DMFT}}_{34}(\nu')` + would land exactly on the pp anti-diagonal :math:`\nu' = -\nu` and corrupt the :math:`\chi^{pp}_0 J` rung. The + loader guarantees this: :func:`~dgamore.local_sde.create_generalized_chi` subtracts that term in the density + channel, and the :math:`\frac{1}{2}(\chi^{ph}_{d} - \chi^{ph}_{m})` combination cancels both it and the + vertical bubble exactly. + + :math:`J` commutes with every pp object by crossing symmetry, so this is the full-space form of inverting the + decoupled singlet/triplet BSEs (thesis Eqs. 3.51/3.52) on their :math:`J`-even/odd blocks. For a single band + :math:`J` reduces to the plain frequency flip and the expression is equivalent to Eq. (B.26) of Rohringer's + thesis. Assumes :math:`G^{\mathrm{DMFT}}_{12}(\nu) = G^{\mathrm{DMFT}}_{21}(\nu)` (real orbital basis, no + spin-orbit coupling); with SOC the rung :math:`\chi^{pp}_0 J` must be replaced by + :math:`-\beta\, G^{\mathrm{DMFT}}_{12}(\nu)\, G^{\mathrm{DMFT}}_{43}(-\nu)\, \delta_{\nu,-\nu'}` (second + Green's function transposed). + + :param gchi_ud_pp_w0: The local connected up-down susceptibility + :math:`\chi^{pp;\nu\nu'}_{\uparrow\downarrow;1234}` in pp notation at :math:`\omega = 0`, see + :meth:`~dgamore.local_four_point.LocalFourPoint.change_frequency_notation_ph_to_pp_w0`. + :param gchi0_pp_w0: The local bare pp bubble :math:`\chi^{pp;\nu\nu'}_{0;1234}` (diagonal in :math:`\nu\nu'`), + built from the DMFT Green's function via + :meth:`~dgamore.bubble_gen.BubbleGenerator.create_generalized_chi0_pp_w0`. + :param beta: Inverse temperature :math:`\beta`, see :attr:`~dgamore.config.SystemConfig.beta`. + :return: The vertex :math:`\Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234}` as a :class:`LocalFourPoint` in pp + notation. + """ + # chi0 * J in tensor form: the bubble with the second fermionic frequency flipped and the orbitals permuted + gchi0_j = gchi0_pp_w0.flip_frequency_axis(-1).permute_orbitals("abcd->adcb", copy=False).to_half_niw_range() + return ( + (gchi0_j - gchi0_pp_w0 @ gchi_ud_pp_w0.invert() @ gchi0_pp_w0) + .invert() + .scale(beta**2) + .set_channel(SpinChannel.UD) + ) + - :param g_dmft: The local (DMFT) :class:`GreensFunction`. +def create_local_gamma_ud_pp_w0_per_ineq( + gchi_ud_pp_w0: LocalFourPoint, g_dmft: GreensFunction, beta: float +) -> LocalFourPoint: + r""" + Builds the local pp-irreducible up-down vertex :math:`\Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234}` per + inequivalent atom and assembles the per-atom blocks into the full multi-band object (mirroring the local + Schwinger-Dyson assembly). Local correlations do not connect orbitals of different atoms, so the assembled + multi-band susceptibility is nonzero only when all four orbital indices belong to the same atom; the compound + pp matrix of the FULL object is therefore singular for more than one atom and must never be inverted directly. + Instead, :func:`create_local_gamma_ud_pp_w0` is evaluated on each atom's orbital block (with the bare pp + bubble built from that atom's block of :math:`G^{\mathrm{DMFT}}_{12}(\nu)`), computing every inequivalent atom + only once and writing the result into all of its positions in the compound band layout. + + :param gchi_ud_pp_w0: The full multi-band connected up-down susceptibility + :math:`\chi^{pp;\nu\nu'}_{\uparrow\downarrow;1234}` in pp notation at :math:`\omega = 0` + (block-structured per inequivalent atom). + :param g_dmft: The full multi-band local DMFT :class:`GreensFunction` :math:`G^{\mathrm{DMFT}}_{12}(\nu)`. + :param beta: Inverse temperature :math:`\beta`. + :return: The assembled vertex :math:`\Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234}` as a + :class:`LocalFourPoint` in pp notation (nonzero only on the same-atom orbital blocks). + """ + n_bands = gchi_ud_pp_w0.n_bands + if config.dmft.n_bands_per_ineq and config.dmft.ineq_ordering: + layout = [] + n_start = 0 + for ineq in config.dmft.ineq_ordering: + n_end = n_start + config.dmft.n_bands_per_ineq[ineq - 1] + layout.append((ineq, slice(n_start, n_end))) + n_start = n_end + else: + layout = [(1, slice(0, n_bands))] + + gamma_full = gchi_ud_pp_w0._clone_without_mat() + gamma_full.mat = np.zeros(gchi_ud_pp_w0.current_shape, dtype=gchi_ud_pp_w0.mat.dtype) + gamma_full.update_original_shape() + + gamma_per_ineq: dict[int, LocalFourPoint] = {} + for ineq, sl in layout: + if ineq not in gamma_per_ineq: + gchi_block = LocalFourPoint( + gchi_ud_pp_w0.mat[sl, sl, sl, sl].copy(), + SpinChannel.UD, + 1, + 2, + gchi_ud_pp_w0.full_niw_range, + gchi_ud_pp_w0.full_niv_range, + FrequencyNotation.PP, + ) + g_mat_block = g_dmft.mat[..., sl, sl, :] + g_block = GreensFunction(g_mat_block.reshape(g_mat_block.shape[-3:]).copy()) + gchi0_block = BubbleGenerator.create_generalized_chi0_pp_w0( + g_block, gchi_block.niv, beta + ).extend_vn_to_diagonal() + gamma_per_ineq[ineq] = create_local_gamma_ud_pp_w0(gchi_block, gchi0_block, beta) + gamma_full.mat[sl, sl, sl, sl] = gamma_per_ineq[ineq].mat + + return gamma_full.set_channel(SpinChannel.UD) + + +def create_local_ud_diagrams_pp_w0( + g_dmft: GreensFunction, niv_pp: int +) -> tuple[LocalFourPoint, LocalFourPoint, LocalFourPoint]: + r""" + Builds the local particle-particle reducible diagrams at :math:`\omega = 0` in the up-down channel: the full + vertex :math:`F^{pp;\nu\nu'}_{\uparrow\downarrow;1234}`, the pp-irreducible vertex + :math:`\Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234}` (built per inequivalent atom and assembled into the full + multi-band object, see :func:`create_local_gamma_ud_pp_w0_per_ineq`), and the reducible part + + .. math:: \Phi^{pp;\nu\nu'}_{\uparrow\downarrow;1234} = F^{pp;\nu\nu'}_{\uparrow\downarrow;1234} + - \Gamma^{pp;\nu\nu'}_{\uparrow\downarrow;1234}, + + with :math:`\chi^{pp}_{\uparrow\downarrow} = \frac{1}{2}(\chi^{ph}_{d} - \chi^{ph}_{m})` mapped to pp notation + at :math:`\omega_{pp} = 0` via :meth:`~dgamore.local_four_point.LocalFourPoint.change_frequency_notation_ph_to_pp_w0` + (ph legs evaluated at :math:`\omega_{ph} = \nu + \nu'`) and the bare pp bubble built from the local DMFT + Green's function :math:`G^{\mathrm{DMFT}}_{12}(\nu)` via + :meth:`~dgamore.bubble_gen.BubbleGenerator.create_generalized_chi0_pp_w0`. These are the local diagrams + subtracted/added when :attr:`~dgamore.config.EliashbergConfig.include_local_part` is enabled, to avoid double + counting the local pairing contribution (thesis Eqs. 4.49-4.52). + + :param g_dmft: The local DMFT :class:`GreensFunction` :math:`G^{\mathrm{DMFT}}_{12}(\nu)`. + :param niv_pp: Number of positive fermionic frequencies of the pp vertex; the local diagrams are cut to this + box so they always match the ladder pairing vertex, also when ``niw_core > niv_core``. :return: The tuple ``(f_ud_loc_pp_w0, gamma_ud_loc_pp_w0, phi_ud_loc_pp_w0)`` of local pp diagrams at :math:`\omega = 0`. """ gchi_dens_loc = LocalFourPoint.load(os.path.join(config.output.output_path, f"gchi_dens_loc.npy"), SpinChannel.DENS) gchi_magn_loc = LocalFourPoint.load(os.path.join(config.output.output_path, f"gchi_magn_loc.npy"), SpinChannel.MAGN) - gchi_ud_loc = 0.5 * (gchi_dens_loc - gchi_magn_loc).set_channel(SpinChannel.UD) - gchi_ud_loc_pp_w0 = gchi_ud_loc.change_frequency_notation_ph_to_pp_w0() + gchi_ud_loc = (gchi_dens_loc - gchi_magn_loc).set_channel(SpinChannel.UD).scale(0.5) + gchi_ud_loc_pp_w0 = gchi_ud_loc.change_frequency_notation_ph_to_pp_w0().cut_niv(niv_pp) del gchi_dens_loc, gchi_magn_loc, gchi_ud_loc - gchi0_loc_pp_w0 = ( - BubbleGenerator.create_generalized_chi0_pp_w0(g_dmft, gchi_ud_loc_pp_w0.niv, config.sys.beta) - .extend_vn_to_diagonal() - .flip_frequency_axis(-1, False) - ) - - gamma_ud_loc_pp_w0 = config.sys.beta**2 * ( - (gchi_ud_loc_pp_w0 - gchi0_loc_pp_w0).invert() + gchi0_loc_pp_w0.invert() - ) + gamma_ud_loc_pp_w0 = create_local_gamma_ud_pp_w0_per_ineq(gchi_ud_loc_pp_w0, g_dmft, config.sys.beta) + del gchi_ud_loc_pp_w0 gamma_ud_loc_pp_w0.save(output_dir=config.output.eliashberg_path, name="gamma_ud_loc_pp_w0") f_dens_loc = LocalFourPoint.load(os.path.join(config.output.output_path, f"f_dens_loc.npy"), SpinChannel.DENS) f_magn_loc = LocalFourPoint.load(os.path.join(config.output.output_path, f"f_magn_loc.npy"), SpinChannel.MAGN) - f_ud_loc = 0.5 * (f_dens_loc - f_magn_loc).set_channel(SpinChannel.UD) - f_ud_loc_pp_w0 = f_ud_loc.change_frequency_notation_ph_to_pp_w0() + f_ud_loc = (f_dens_loc - f_magn_loc).set_channel(SpinChannel.UD).scale(0.5) + f_ud_loc_pp_w0 = f_ud_loc.change_frequency_notation_ph_to_pp_w0().cut_niv(niv_pp) del f_dens_loc, f_magn_loc, f_ud_loc @@ -475,6 +656,77 @@ def _chi0_to_matmul_layout(chi0_mat: np.ndarray) -> np.ndarray: return np.moveaxis(chi0_mat.reshape(nqx, nqy, nqz, nb * nb, nb * nb, v), -1, 3) +def symmetrize_degenerate_gaps( + lambdas: np.ndarray, gaps: np.ndarray, gap_shape: tuple, tol: float = 1e-4 +) -> np.ndarray: + r""" + Orthonormalizes the eigenvectors returned by the Lanczos solver within clusters of (near-)degenerate + eigenvalues and rotates every two-dimensional cluster to the mirror-adapted basis. The pairing kernel is only + symmetrizable, not Hermitian in the plain inner product, so ARPACK may return oblique (mutually + non-orthogonal) combinations inside a degenerate doublet: the doublet subspace is symmetry-covariant, but the + two returned vectors then do not form 90-degree-rotated partners. + + Per cluster the following steps are applied: (i) Loewdin orthonormalization, i.e. :math:`S^{-1/2}` applied to + the cluster overlap matrix :math:`S`, which yields the orthonormal basis closest to the input vectors; (ii) + for doublets, the mirror operation + + .. math:: M_y: \Delta_{12}(k_x, k_y, k_z, \nu) \to \Delta_{12}(k_x, -k_y, k_z, \nu) + + is diagonalized within the cluster, ordering the even (:math:`+1`, :math:`p_x`-like) partner first and the + odd (:math:`-1`, :math:`p_y`-like) partner second; (iii) the global phase of every vector is fixed such that + its largest-magnitude element is real and positive. Eigenvalues are not modified; vectors of non-degenerate + eigenvalues are only phase-fixed. Enabled via + :attr:`~dgamore.config.EliashbergConfig.symmetrize_degenerate_gaps`. + + :param lambdas: Eigenvalues sorted in descending order. + :param gaps: Eigenvector matrix ``[n, n_eig]`` with one flattened gap function per column. + :param gap_shape: Full gap shape ``[kx, ky, kz, o1, o2, 2*niv_pp]``, used to locate the :math:`k_y` axis. + :param tol: Relative tolerance for clustering neighboring eigenvalues as degenerate. + :return: The symmetrized eigenvector matrix ``[n, n_eig]``. + """ + n_ky = gap_shape[1] + idx_neg = (n_ky - np.arange(n_ky)) % n_ky + + def mirror_y(column: np.ndarray) -> np.ndarray: + return column.reshape(gap_shape)[:, idx_neg].ravel() + + clusters = [[0]] + for i in range(1, gaps.shape[1]): + if abs(lambdas[i] - lambdas[i - 1]) <= tol * max(abs(lambdas[i]), 1e-12): + clusters[-1].append(i) + else: + clusters.append([i]) + + gaps = gaps.copy() + for cluster in clusters: + block = gaps[:, cluster].astype(np.complex128) + block /= np.linalg.norm(block, axis=0) + + if len(cluster) > 1: + overlap = block.conj().T @ block + eigs, u = np.linalg.eigh(overlap) + if eigs.min() < 1e-12: # (nearly) linearly dependent vectors cannot be orthonormalized meaningfully + continue + block = block @ (u @ np.diag(eigs**-0.5) @ u.conj().T) + + if len(cluster) == 2: + mirrored = np.stack([mirror_y(block[:, i]) for i in range(2)], axis=1) + mirror_block = block.conj().T @ mirrored + mirror_block = 0.5 * (mirror_block + mirror_block.conj().T) + _, mirror_vecs = np.linalg.eigh(mirror_block) + # order the even (+1, p_x-like) partner first and the odd (-1, p_y-like) partner second + block = block @ mirror_vecs[:, ::-1] + + for col in range(block.shape[1]): + mags = np.abs(block[:, col]) + # tie-break on the first index among the maximal-modulus elements, stable against fp noise + phase = block[np.flatnonzero(mags >= mags.max() * (1.0 - 1e-8))[0], col] + block[:, col] *= phase.conjugate() / abs(phase) + gaps[:, cluster] = block + + return gaps + + def _apply_gchi0_pp(chi0_mm: np.ndarray, gap: np.ndarray, n_bands: int) -> np.ndarray: r""" Batched-matmul equivalent of ``np.einsum("xyzabcdv,xyzcdv->xyzabv", chi0, gap)`` (multiply the gap by the bare pp @@ -562,9 +814,6 @@ def solve_eliashberg_lanczos( gchi0_q0_pp = gchi0_q0_pp.decompress_q_dimension() gap0 = get_initial_gap_function(gap_shape, gamma_r_pp.channel) - gap0 = gap0.reshape((np.prod(gamma_r_pp.nq),) + 2 * (gamma_r_pp.n_bands,) + (2 * gamma_r_pp.niv,))[ - config.lattice.q_grid.irrk_ind - ][config.lattice.q_grid.irrk_inv].reshape(gap_shape) symmetry_label = config.eliashberg.symmetry.lower() if config.eliashberg.symmetry else "random" logger.info( f"Initialized the gap function as {symmetry_label} for the {gamma_r_pp.channel.value}let channel.", @@ -579,9 +828,12 @@ def solve_eliashberg_lanczos( # is freed right after its matmul-layout copy is built (peak stays ~1 vertex, not 3), and the flipped-term sign is # folded in place. chi0_mm = _chi0_to_matmul_layout(gchi0_q0_pp.mat) - gamma_mm = _gamma_to_matmul_layout(gamma_r_pp.mat) + # The pairing vertex arrives in the w2dynamics G2 leg order (c cdag c cdag), whereas the contraction in + # _apply_gamma_pp expects the TRIQS order (cdag c cdag c), see + # https://triqs.github.io/tprf/latest/theory/eliashberg.html + gamma_mm = _gamma_to_matmul_layout(gamma_r_pp.permute_orbitals("abcd->badc", False).mat) gamma_r_pp.free() - gamma_flipped_mm = _gamma_to_matmul_layout(gamma_pp_flipped.mat) + gamma_flipped_mm = _gamma_to_matmul_layout(gamma_pp_flipped.permute_orbitals("abcd->badc", False).mat) gamma_pp_flipped.free() gamma_flipped_mm *= sign @@ -629,6 +881,9 @@ def mv(gap: np.ndarray): lambdas = lambdas[order] gaps = gaps[:, order] + if config.eliashberg.symmetrize_degenerate_gaps: + gaps = symmetrize_degenerate_gaps(lambdas, gaps, gap_shape) + logger.info( f"Largest{eig_label} eigenvalue{plural} for the {gamma_r_pp.channel.value}let " f"channel {"is" if n_eig == 1 else "are"}: " + ", ".join(f"{lam:.6f}" for lam in lambdas), @@ -698,9 +953,13 @@ def solve_eliashberg_lanczos_v2( # pairing vertices materialized once per rank. Each einsum-layout source is freed right after its matmul-layout copy # is built (peak stays ~1 vertex, not 3), and the flipped-term sign is folded in place. chi0_mm = _chi0_to_matmul_layout(gchi0_q0_pp.mat) if mpi_dist_v.comm.rank == root else None - gamma_mm = _gamma_to_matmul_layout(gamma_r_pp.mat) + # The pairing vertex arrives in the w2dynamics G2 leg order (c cdag c cdag), whereas the contraction in + # _apply_gamma_pp expects the TRIQS order (cdag c cdag c). The two differ by the "abcd->badc" swap + # (o1<->o2, o3<->o4); applying it to both the direct and the flipped vertex makes the gap's creation legs + # contract with the vertex's creation legs. This is a no-op for a single band. + gamma_mm = _gamma_to_matmul_layout(gamma_r_pp.permute_orbitals("abcd->badc", False).mat) gamma_r_pp.free() - gamma_flipped_mm = _gamma_to_matmul_layout(gamma_r_pp_flipped.mat) + gamma_flipped_mm = _gamma_to_matmul_layout(gamma_r_pp_flipped.permute_orbitals("abcd->badc", False).mat) gamma_r_pp_flipped.free() gamma_flipped_mm *= sign @@ -757,6 +1016,9 @@ def mv(gap: np.ndarray): lambdas = lambdas[order] gaps = gaps[:, order] + if config.eliashberg.symmetrize_degenerate_gaps: + gaps = symmetrize_degenerate_gaps(lambdas, gaps, gap_shape) + logger.info( f"Largest{eig_label} eigenvalue{plural} for the {gamma_r_pp.channel.value}let " f"channel {"is" if n_eig == 1 else "are"}: " + ", ".join(f"{lam:.6f}" for lam in lambdas), @@ -776,6 +1038,47 @@ def mv(gap: np.ndarray): return lambdas, gaps +def dispatch_full_vertex_calculation( + channel: SpinChannel, u_loc: LocalInteraction, v_nonloc: Interaction, niv_pp: int, mpi_dist: MpiDistributor +) -> FourPoint: + r""" + Loads the local irreducible vertex for ``channel`` and builds the full ladder pp vertex, dispatching between + the memory-lean and the regular construction routine based on the memory configuration. Please note that + Eq. (4.43) in my master's thesis is wrong. The correct formula is + :math:`F^{q\nu\nu'}_{r;1234}=F^{(1);q\nu\nu'}_{r;1234}+F^{(2);q\nu\nu'}_{r;1234}`, with + :math:`F^{(1);q\nu\nu'}_{r;1234} = \beta^2\Big[(\chi_{0;1234}^{q\nu\nu'})^{-1}- + \sum_{\nu_1\nu_2}\sum_{abcd}(\chi_{0;12ab}^{q\nu\nu_1})^{-1}\chi_{r;bacd}^{*;q\nu_1\nu_2}(\chi_{0;dc34}^{q\nu_2\nu'})^{-1}\Big]` and + :math:`F^{(2);q\nu\nu'}_{r;1234} = \sum_{abcdgh}\gamma^{q\nu}_{r;12ab}\Big(\mathbb{1}_{bacd} - + \sum_{ef}\mathcal{U}^{q}_{r;baef}\chi^{q}_{r;fecd}\Big)\mathcal{U}^{q}_{r;dcgh}\tilde\gamma^{q\nu'}_{r;hg34}`, + where + :math:`\tilde\gamma_{r;1234}^{q\nu}=\beta \sum_{ab}\sum_{\nu'} \chi^{*;q\nu'\nu}_{r;12ab} (\chi^{q\nu}_{0;ba34})^{-1} + =\beta \sum_{ab}\sum_{\nu'} \chi^{*;q\nu\nu'}_{r;ab21} (\chi^{q\nu}_{0;ab34})^{-1}`, i.e. the sum over the first + frequency argument equals the sum over the last one only up to the orbital reversal dictated by + time-reversal symmetry, see :meth:`~dgamore.nonlocal_sde.create_vrg_r_q_right`. No explicit factors of + :math:`\beta` appear in :math:`F^{(2)}` because they are absorbed into the stored objects: + :math:`\chi^{q}_{r}` is the (:math:`U`-dressed, shell- (and sometimes :math:`\lambda`-corrected)) physical + susceptibility normalized as :math:`\frac{1}{\beta^2}\sum_{\nu\nu'}\chi^{q\nu\nu'}_{r}`, and the three-leg + vertices carry the net normalization :math:`\gamma^{q\nu}_{r} = (\chi^{q\nu}_{0})^{-1}\sum_{\nu'} + \chi^{*;q\nu\nu'}_{r}` (the explicit :math:`\beta` in their construction cancels the :math:`1/\beta` of the + fused frequency sum), such that :math:`\gamma^{q\nu}_{r} \to \mathbb{1}` for :math:`\nu \to \infty`. + + :param channel: The spin channel (density or magnetic). + :param u_loc: The bare local interaction :math:`U`. + :param v_nonloc: The non-local interaction :math:`V^{q}`. + :param niv_pp: Number of positive fermionic frequencies of the pp vertex. + :param mpi_dist: MPI distributor over the irreducible BZ q-points. + :return: The full ladder pp vertex :math:`F^{q}_{r}` as a :class:`FourPoint`. + """ + gamma_r = LocalFourPoint.load(os.path.join(config.output.output_path, f"gamma_{channel.value}_loc.npy"), channel) + if config.memory.save_memory_for_fq: + f_q_r = create_full_vertex_q_r_pp_w0_v2(u_loc, v_nonloc, gamma_r, niv_pp, mpi_dist) + else: + f_q_r = create_full_vertex_q_r_pp_w0(u_loc, v_nonloc, gamma_r, niv_pp, mpi_dist) + gamma_r.free() + mpi_dist.barrier() + return f_q_r + + def solve( giwk_dga: GreensFunction, g_dmft: GreensFunction, u_loc: LocalInteraction, v_nonloc: Interaction, comm: MPI.Comm ): @@ -805,29 +1108,6 @@ def solve( niv_pp = min(config.box.niw_core // 2, config.box.niv_core // 2) - def dispatch_full_vertex_calculation(channel, u, v, niv, mpi_dist) -> FourPoint: - r""" - Loads the local irreducible vertex for ``channel`` and builds the full ladder pp vertex, dispatching between - the memory-lean and the regular construction routine based on the memory configuration. - - :param channel: The spin channel (density or magnetic). - :param u: The bare local interaction :math:`U`. - :param v: The non-local interaction :math:`V^{q}`. - :param niv: Number of positive fermionic frequencies of the pp vertex. - :param mpi_dist: MPI distributor over the irreducible BZ q-points. - :return: The full ladder pp vertex :math:`F^{q}_{r}` as a :class:`FourPoint`. - """ - gamma_r = LocalFourPoint.load( - os.path.join(config.output.output_path, f"gamma_{channel.value}_loc.npy"), channel - ) - if config.memory.save_memory_for_fq: - f_q_r = create_full_vertex_q_r_pp_w0_v2(u, v, gamma_r, niv, mpi_dist) - else: - f_q_r = create_full_vertex_q_r_pp_w0(u, v, gamma_r, niv, mpi_dist) - gamma_r.free() - mpi_dist.barrier() - return f_q_r - f_dens_pp = dispatch_full_vertex_calculation(SpinChannel.DENS, u_loc, v_nonloc, niv_pp, mpi_dist_irrk) f_magn_pp = dispatch_full_vertex_calculation(SpinChannel.MAGN, u_loc, v_nonloc, niv_pp, mpi_dist_irrk) @@ -847,7 +1127,7 @@ def dispatch_full_vertex_calculation(channel, u, v, niv, mpi_dist) -> FourPoint: logger.info("Calculated full ladder-vertex (triplet) in pp notation.") if config.eliashberg.include_local_part: - f_ud_loc_pp_w0, gamma_ud_loc_pp_w0, phi_ud_loc_pp_w0 = create_local_ud_diagrams_pp_w0(g_dmft) + f_ud_loc_pp_w0, gamma_ud_loc_pp_w0, phi_ud_loc_pp_w0 = create_local_ud_diagrams_pp_w0(g_dmft, niv_pp) if mpi_dist_irrk.my_rank == 0: f_ud_loc_pp_w0.save(output_dir=config.output.eliashberg_path, name="f_ud_loc_pp_w0") @@ -861,12 +1141,18 @@ def dispatch_full_vertex_calculation(channel, u, v, niv, mpi_dist) -> FourPoint: # different from the regular pp f_dens_loc = LocalFourPoint.load(os.path.join(config.output.output_path, f"f_dens_loc.npy"), SpinChannel.DENS) f_magn_loc = LocalFourPoint.load(os.path.join(config.output.output_path, f"f_magn_loc.npy"), SpinChannel.MAGN) - f_ud_loc = 0.5 * (f_dens_loc - f_magn_loc).set_channel(SpinChannel.UD) + f_ud_loc = (f_dens_loc - f_magn_loc).set_channel(SpinChannel.UD).scale(0.5) f_ud_loc_transf_w0 = transform_vertex_loc_frequencies_w0(f_ud_loc, niv_pp) del f_dens_loc, f_magn_loc, f_ud_loc - gamma_sing_pp += f_ud_loc_transf_w0 + phi_ud_loc_pp_w0 - gamma_trip_pp += f_ud_loc_transf_w0 + phi_ud_loc_pp_w0 + # Eqs. (4.49)-(4.52): the assembled vertex holds the negative crossed slot, so the local full vertex enters + # with a relative minus (the transform already carries one minus) and the local pp-reducible diagrams phi + # enter with a plus, both in crossed-slot form (frequencies (v, -v'), orbitals 1432). + phi_ud_loc_pp_w0 = phi_ud_loc_pp_w0.flip_frequency_axis(-1, copy=False).permute_orbitals( + "abcd->adcb", copy=False + ) + gamma_sing_pp += phi_ud_loc_pp_w0 - f_ud_loc_transf_w0 + gamma_trip_pp += phi_ud_loc_pp_w0 - f_ud_loc_transf_w0 del phi_ud_loc_pp_w0, f_ud_loc_transf_w0 if config.eliashberg.save_pairing_vertex: @@ -949,7 +1235,7 @@ def dispatch_full_vertex_calculation(channel, u, v, niv, mpi_dist) -> FourPoint: lambdas_sing, gaps_sing = solve_eliashberg_lanczos_v2(gamma_sing_pp, gchi0_q_pp, mpi_dist_v, active_ranks) gamma_sing_pp.free() else: - lambdas_sing, gaps_sing = None, None + lambdas_sing = None logger.info("Distributing Gamma_trip_pp along v equally to ranks/nodes.") gamma_trip_pp = mpi_utils.gather_full_ibz_for_vslice( @@ -961,7 +1247,7 @@ def dispatch_full_vertex_calculation(channel, u, v, niv, mpi_dist) -> FourPoint: lambdas_trip, gaps_trip = solve_eliashberg_lanczos_v2(gamma_trip_pp, gchi0_q_pp, mpi_dist_v, active_ranks) gamma_trip_pp.free() else: - lambdas_trip, gaps_trip = None, None + lambdas_trip = None lambdas_sing = comm.bcast(lambdas_sing, root=root) lambdas_trip = comm.bcast(lambdas_trip, root=root) diff --git a/dgamore/four_point.py b/dgamore/four_point.py index ee8acc9a..6a2bbcce 100644 --- a/dgamore/four_point.py +++ b/dgamore/four_point.py @@ -1,19 +1,18 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Momentum-dependent four-point objects. :class:`FourPoint` extends :class:`LocalFourPoint` with one momentum axis (see :class:`IAmNonLocal`) to represent quantities such as the ladder susceptibility -:math:`\chi_{abcd}^{q\omega\nu\nu'}` and vertex :math:`F^{q}` over the (irreducible) BZ. The arithmetic and +:math:`\chi_{1234}^{q\omega\nu\nu'}` and vertex :math:`F^{q}` over the (irreducible) BZ. The arithmetic and compound-index machinery mirrors :class:`LocalFourPoint` but accounts for the extra momentum dimension; these operations are the performance and memory bottleneck of the non-local ladder DGA step, so several variants are provided that trade speed for footprint. Notation mirrors the thesis (Chapters 3 & 4). """ import gc -from copy import deepcopy import numpy as np @@ -90,7 +89,7 @@ def __rsub__(self, other) -> "FourPoint": """ Reflected operator form of :meth:`sub` (``B - A``), returning ``-(A - B)``. See :meth:`sub`. """ - return -self.sub(other) + return self.sub(other).scale(-1.0) def __mul__(self, other) -> "FourPoint": """ @@ -124,9 +123,9 @@ def __invert__(self): def __pow__(self, power, modulo=None) -> "FourPoint": """ - Operator form of :meth:`pow` (``A ** n``), supplying the matching identity. See :meth:`pow`. + Operator form of :meth:`pow` (``A ** n``). See :meth:`pow`. """ - return self.pow(power, FourPoint.identity_like(self)) + return self.pow(power) def sum_over_vn(self, beta: float, axis: tuple = (-1,), copy: bool = True) -> "FourPoint": r""" @@ -196,7 +195,7 @@ def sum_over_orbitals(self, orbital_contraction: str = "abcd->ad") -> "FourPoint def to_compound_indices(self) -> "FourPoint": r""" - Converts the indices of the FourPoint object :math:`F^{wvv'}_{lmm'l'}` to compound indices :math:`F^{w}_{c_1, c_2}` + Converts the indices of the FourPoint object :math:`F^{\omega\nu\nu'}_{1234}` to compound indices :math:`F^{\omega}_{c_1, c_2}` by transposing the object to [q, w, o1, o2, v, o4, o3, v'] (if the object has any fermionic frequency dimension, otherwise the compound indices are built from orbital dimensions only) and grouping {o1, o2, v} and {o4, o3, v'} to the new compound index. Always returns the object with a compressed momentum dimension and in the same niw @@ -266,7 +265,7 @@ def _to_compound_indices_pp(self) -> "FourPoint": :return: ``self`` in compound-index layout. """ - if len(self.current_shape) == 3: # [q, w, x1, x2] + if len(self.current_shape) == 3 + self.num_wn_dimensions: # [q, w, x1, x2] or [q, x1, x2] return self return self.permute_orbitals("abcd->acbd", copy=False)._to_compound_indices_ph() @@ -371,7 +370,7 @@ def permute_orbitals(self, permutation: str = "abcd->abcd", copy: bool = True) - return self if copy: - return deepcopy(self).permute_orbitals(permutation, copy=False) + return self.copy().permute_orbitals(permutation, copy=False) permutation = ( f"i{split[0]}...->i{split[1]}..." @@ -392,18 +391,20 @@ def map_to_full_bz(self, grid: KGrid, nq: tuple = None): """ return self._map_to_full_bz(grid, 4, nq) - def add(self, other) -> "FourPoint": + def add(self, other, copy: bool = True) -> "FourPoint": """ Adds ``other`` to this object (operator ``+``); see :meth:`_add` for the accepted operands and the niw-range handling. :param other: A :class:`FourPoint`, :class:`LocalFourPoint`, :class:`Interaction`, :class:`LocalInteraction`, numpy array, or number. - :return: A new :class:`FourPoint` holding the sum. + :param copy: If True (default), return a new :class:`FourPoint`; if False, accumulate into ``self`` in place + (only supported when ``other`` is a conforming :class:`FourPoint`, see :meth:`_add`). + :return: A new :class:`FourPoint` holding the sum (or ``self`` when ``copy=False``). """ - return self._add(other) + return self._add(other, copy=copy) - def _add(self, other, subtract: bool = False) -> "FourPoint": + def _add(self, other, subtract: bool = False, copy: bool = True) -> "FourPoint": """ Helper method that allows for addition of FourPoint objects and other FourPoint, LocalFourPoint, Interaction or LocalInteraction objects. Additions with numpy arrays, floats, ints or complex numbers are also supported. @@ -414,8 +415,16 @@ def _add(self, other, subtract: bool = False) -> "FourPoint": :param other: A :class:`FourPoint`, :class:`LocalFourPoint`, :class:`Interaction`, :class:`LocalInteraction`, numpy array, or number. Local operands are broadcast over the momentum axis. :param subtract: If True, subtract ``other`` instead of adding it (used by :meth:`sub` to avoid a negated copy). - :return: A new :class:`FourPoint` (in the half niw range for the vertex-vertex case). - :raises ValueError: If ``other`` has an unsupported type. + :param copy: If True (default), return a new :class:`FourPoint`; if False, accumulate the result into + ``self.mat`` in place and return ``self`` (no out-of-place result block). The in-place branch is only + supported between two :class:`FourPoint` objects whose fermionic frequency dimensions already match (it + refuses to diagonally extend ``self``), which is the self-energy-kernel accumulation case in + :mod:`dgamore.nonlocal_sde`. + :return: A new :class:`FourPoint` (in the half niw range for the vertex-vertex case), or ``self`` when + ``copy=False``. + :raises ValueError: If ``other`` has an unsupported type, or ``copy=False`` would have to diagonally extend + ``self``. + :raises NotImplementedError: If ``copy=False`` and ``other`` is not a :class:`FourPoint`. """ if not isinstance( other, (FourPoint, LocalFourPoint, Interaction, LocalInteraction, np.ndarray, float, int, complex) @@ -424,6 +433,11 @@ def _add(self, other, subtract: bool = False) -> "FourPoint": op = np.subtract if subtract else np.add + if not copy and not isinstance(other, FourPoint): + raise NotImplementedError( + "In-place addition/subtraction (copy=False) is only supported between two FourPoint objects." + ) + if isinstance(other, (np.ndarray, float, int, complex)): return FourPoint( op(self.mat, other), @@ -491,6 +505,23 @@ def _add(self, other, subtract: bool = False) -> "FourPoint": other = self._align_q_dimensions_for_operations(other) other, self_extended, other_extended = self._align_frequency_dimensions_for_operation(other) + if not copy: + if self_extended: + raise ValueError( + "In-place addition/subtraction (copy=False) cannot diagonally extend 'self'; both operands " + "must have the same number of fermionic frequency dimensions." + ) + # accumulate into self in place: no out-of-place result block (and the caller folds any scalar prefactor + # via FourPoint.scale, so no negated/scaled copy of ``other`` either). + op(self.mat, other.mat, out=self.mat) + self.channel = channel + self._full_niw_range = False + self.update_original_shape() + if other_full_niw_range: + other = other.to_full_niw_range() + self._revert_frequency_dimensions_after_operation(other, other_extended, False) + return self + result = FourPoint( op(self.mat, other.mat), channel, @@ -511,7 +542,7 @@ def _add(self, other, subtract: bool = False) -> "FourPoint": other = self._revert_frequency_dimensions_after_operation(other, other_extended, self_extended) return result - def sub(self, other) -> "FourPoint": + def sub(self, other, copy: bool = True) -> "FourPoint": """ Helper method that allows for subtraction of FourPoint objects and other FourPoint, LocalFourPoint, Interaction or LocalInteraction objects. Subtractions with numpy arrays, floats, ints or complex numbers are also supported. @@ -521,17 +552,20 @@ def sub(self, other) -> "FourPoint": :param other: A :class:`FourPoint`, :class:`LocalFourPoint`, :class:`Interaction`, :class:`LocalInteraction`, numpy array, or number. - :return: The difference, implemented as ``self._add(other, subtract=True)`` (see :meth:`_add`). + :param copy: If True (default), return a new :class:`FourPoint`; if False, subtract into ``self`` in place + and return ``self`` (only supported when ``other`` is a conforming :class:`FourPoint`, see :meth:`_add`). + :return: The difference, implemented as ``self._add(other, subtract=True)`` (see :meth:`_add`), or ``self`` + when ``copy=False``. :raises ValueError: Propagated from :meth:`_add` for unsupported operands. """ - return self._add(other, subtract=True) + return self._add(other, subtract=True, copy=copy) def mul(self, other) -> "FourPoint": r""" Allows for the multiplication with a number, a numpy array or another FourPoint object. This is different from the :meth:`matmul` method, which is used for matrix multiplication. In the case the other object is a FourPoint object, we require that both objects have only one fermionic - frequency dimension, such that :math:`A_{abcd}^{qv} * B_{dcef}^{qv'} = C_{abef}^{qvv'}`. This is needed to + frequency dimension, such that :math:`\sum_{ab} A_{12ab}^{q\nu} * B_{ba34}^{q\nu'} = C_{1234}^{q\nu\nu'}`. This is needed to construct the full vertex, see Eq. (3.139) in my thesis. Returns the object in the half niw range. :param other: A number, numpy array, or :class:`FourPoint`. @@ -543,7 +577,7 @@ def mul(self, other) -> "FourPoint": raise ValueError("Multiplication only supported with numbers, numpy arrays or FourPoint objects.") if not isinstance(other, FourPoint): - copy = deepcopy(self) + copy = self.copy() copy.mat *= other return copy @@ -576,21 +610,29 @@ def matmul(self, other, left_hand_side: bool = True) -> "FourPoint": :param other: A :class:`FourPoint`, :class:`LocalFourPoint`, :class:`Interaction`, or :class:`LocalInteraction`. Local operands are broadcast over the momentum axis. :param left_hand_side: If True, compute ``self @ other``; if False, compute ``other @ self``. - :return: A new :class:`FourPoint` in the half bosonic frequency range, carrying the non-NONE channel. - :raises ValueError: If ``other`` has an unsupported type. + :return: A new :class:`FourPoint` in the half bosonic frequency range, carrying the non-NONE channel and the + :attr:`~dgamore.n_point_base.IHaveChannel.frequency_notation` of ``self``. All branches contract in + the compound space of that notation (ph: rows {1,2,v}, cols {4,3,v'}; pp: rows {1,3,v}, cols + {4,2,v'}; see :meth:`to_compound_indices`). + :raises ValueError: If ``other`` has an unsupported type, or if the operands' frequency notations differ. """ if not isinstance(other, (FourPoint, LocalFourPoint, Interaction, LocalInteraction)): raise ValueError(f"Multiplication {type(self)} @ {type(other)} not supported.") + if isinstance(other, LocalFourPoint) and other.frequency_notation != self.frequency_notation: + raise ValueError("Cannot multiply two objects with different frequency notations.") + if isinstance(other, (LocalInteraction, Interaction)): is_local = not isinstance(other, Interaction) q_prefix = "" if is_local else "q" self.compress_q_dimension() - left_orbs = "abij" - right_orbs = "jief" - final_orbs = "abef" + left_orbs, right_orbs, final_orbs = ( + ("abij", "jief", "abef") + if self.frequency_notation == FrequencyNotation.PH + else ("afce", "ebfd", "abcd") + ) suffix = {0: "w", 1: "wv", 2: "wvp"}.get(self.num_vn_dimensions, "") einsum_str = ( f"q{left_orbs}{suffix},{q_prefix}{right_orbs}->q{final_orbs}{suffix}" @@ -632,10 +674,15 @@ def matmul(self, other, left_hand_side: bool = True) -> "FourPoint": suffix_other, suffix_result, suffix_self = self._get_frequency_suffixes_for_matmul(other, left_hand_side) + left_orbs, right_orbs, final_orbs = ( + ("abcd", "dcef", "abef") + if self.frequency_notation == FrequencyNotation.PH + else ("afce", "ebfd", "abcd") + ) einsum_str = ( - f"qabcd{suffix_self},{q_prefix}dcef{suffix_other}->qabef{suffix_result}" + f"q{left_orbs}{suffix_self},{q_prefix}{right_orbs}{suffix_other}->q{final_orbs}{suffix_result}" if left_hand_side - else f"{q_prefix}abcd{suffix_other},qdcef{suffix_self}->qabef{suffix_result}" + else f"{q_prefix}{left_orbs}{suffix_other},q{right_orbs}{suffix_self}->q{final_orbs}{suffix_result}" ) return FourPoint( @@ -696,7 +743,7 @@ def invert(self, copy: bool = True): """ if copy: - return deepcopy(self).invert(copy=False) + return self.copy().invert(copy=False) self.to_half_niw_range() if self.num_vn_dimensions == 1: diff --git a/dgamore/gap_function.py b/dgamore/gap_function.py index 916c50ff..ab5bf3be 100644 --- a/dgamore/gap_function.py +++ b/dgamore/gap_function.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" The superconducting gap function :math:`\Delta`, i.e. the eigenvector of the linearized Eliashberg equation. diff --git a/dgamore/greens_function.py b/dgamore/greens_function.py index 7e38e605..e55861b2 100644 --- a/dgamore/greens_function.py +++ b/dgamore/greens_function.py @@ -1,11 +1,11 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Single-particle Green's function. :class:`GreensFunction` builds the momentum-dependent interacting Green's -function :math:`G_{ab}(k, \nu) = [(\imath\nu + \mu)\delta_{ab} - \varepsilon_{ab}(k) - \Sigma_{ab}(k, \nu)]^{-1}` +function :math:`G_{12}(k, \nu) = [(\imath\nu + \mu)\delta_{12} - \varepsilon_{12}(k) - \Sigma_{12}(k, \nu)]^{-1}` from a :class:`SelfEnergy`, the band dispersion :math:`\varepsilon(k)` and the chemical potential :math:`\mu`, and derives the filling, occupation, kinetic and (Galitskii-Migdal) potential energies. The module-level helpers adjust :math:`\mu` to a target filling via a Newton root search. Moment-corrected asymptotic sums are used so the @@ -99,7 +99,12 @@ def root_fun( def update_mu( - mu0: float, target_filling: float, ek: np.ndarray, sigma_mat: np.ndarray, beta: float, smom0: np.ndarray, + mu0: float, + target_filling: float, + ek: np.ndarray, + sigma_mat: np.ndarray, + beta: float, + smom0: np.ndarray, logger=None, ) -> float: r""" @@ -133,11 +138,11 @@ def update_mu( class GreensFunction(TwoPoint): r""" - The single-particle Green's function :math:`G_{ab}(k, \nu) = [(\imath\nu + \mu)\delta_{ab} - - \varepsilon_{ab}(k) - \Sigma_{ab}(k, \nu)]^{-1}`. Built from a :class:`SelfEnergy`, the band dispersion + The single-particle Green's function :math:`G_{12}(k, \nu) = [(\imath\nu + \mu)\delta_{12} - + \varepsilon_{12}(k) - \Sigma_{12}(k, \nu)]^{-1}`. Built from a :class:`SelfEnergy`, the band dispersion :math:`\varepsilon(k)` and the chemical potential :math:`\mu`; on top of the two-point orbital bookkeeping inherited from :class:`LocalTwoPoint` it adds the Dyson construction (local and momentum-resolved) and the - derived quantities — filling, occupation matrices, kinetic and (Galitskii-Migdal) potential energy — all using + derived quantities - filling, occupation matrices, kinetic and (Galitskii-Migdal) potential energy - all using moment-corrected asymptotic Matsubara sums so the finite frequency box does not bias the result. """ @@ -245,7 +250,9 @@ def get_g_full(siw: SelfEnergy, mu: float, ek: np.ndarray, beta: float): return GreensFunction(mat, siw, ek, siw.full_niv_range, False, False, nk=ek.shape[:3], beta=beta, mu=mu) @staticmethod - def create_g_loc(siw: SelfEnergy, ek: np.ndarray, beta: float, mu: float, calc_filling: bool = True) -> "GreensFunction": + def create_g_loc( + siw: SelfEnergy, ek: np.ndarray, beta: float, mu: float, calc_filling: bool = True + ) -> "GreensFunction": r""" Builds a local (k-summed) Green's function from a self-energy and band dispersion. @@ -276,7 +283,7 @@ def _invert_last_orbital_block(mat: np.ndarray) -> np.ndarray: def get_g_wv(self, wn: np.ndarray, niv_cut: int) -> np.ndarray: r""" - Returns the frequency-shifted Green's function :math:`G_{ab}^{\nu - \omega}` on a fermionic window of half + Returns the frequency-shifted Green's function :math:`G_{12}^{\nu - \omega}` on a fermionic window of half width ``niv_cut``, for the bosonic frequencies in ``wn``. :param wn: Array of bosonic Matsubara indices :math:`\omega`. @@ -313,7 +320,7 @@ def get_fill_nonlocal(self) -> tuple[float, np.ndarray, np.ndarray]: def get_ekin(self) -> float: r""" Returns the kinetic energy from the band dispersion and the k-resolved occupation, - :math:`E_{kin} = \sum_{\sigma \vec{k} ab} \varepsilon(\vec{k})_{ab}\, n(\vec{k})_{ba}`. + :math:`E_{kin} = \sum_{\sigma \vec{k} ab} \varepsilon_{ab}(\vec{k})\, n_{ba}(\vec{k})`. :return: The kinetic energy per site. """ diff --git a/dgamore/hamiltonian.py b/dgamore/hamiltonian.py index 2d573330..fdc335b1 100644 --- a/dgamore/hamiltonian.py +++ b/dgamore/hamiltonian.py @@ -1,14 +1,14 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Lattice model setup. :class:`Hamiltonian` assembles the (multi-orbital) Hubbard model: the real-space hopping -:math:`t_{ab}(R)`, the local interaction :math:`U_{abcd}` and the non-local interaction :math:`V_{abcd}(R)`. It +:math:`t_{12}(R)`, the local interaction :math:`U_{1234}` and the non-local interaction :math:`V_{1234}(R)`. It offers convenience builders (single-band, orbital-diagonal, Kanamori, 2D :math:`t`-:math:`t'`-:math:`t''`), readers/writers for wannier90 / wien2k ``hr``/``hk`` and ``umatrix`` files, and Fourier transforms to obtain the -band dispersion :math:`\varepsilon_{ab}(k)` and momentum-dependent interaction :math:`V_{abcd}(q)`. +band dispersion :math:`\varepsilon_{12}(k)` and momentum-dependent interaction :math:`V_{1234}(q)`. :class:`HoppingElement` and :class:`InteractionElement` are small validated containers for single hopping/ interaction entries. """ @@ -501,7 +501,7 @@ def read_umatrix(self, filename: str) -> "Hamiltonian": def get_ek(self, k_grid: bz.KGrid = None) -> np.ndarray: r""" - Returns the band dispersion :math:`\varepsilon_{ab}(k)`, computing and caching it from the real-space hopping + Returns the band dispersion :math:`\varepsilon_{12}(k)`, computing and caching it from the real-space hopping on ``k_grid`` if not already set. :param k_grid: The :class:`KGrid` to evaluate on (ignored if the dispersion is already cached/set). @@ -535,7 +535,7 @@ def get_local_u(self) -> LocalInteraction: def get_vq(self, q_grid: bz.KGrid) -> "Interaction": r""" - Returns the momentum-dependent non-local interaction :math:`V_{abcd}(q)` by Fourier-transforming the + Returns the momentum-dependent non-local interaction :math:`V_{1234}(q)` by Fourier-transforming the real-space interaction onto ``q_grid``. :param q_grid: The :class:`KGrid` defining the momentum grid. @@ -703,7 +703,7 @@ def _insert_ur_element( def _convham_2_orbs(self, k_mesh: np.ndarray) -> np.ndarray: r""" - Fourier-transforms the real-space hopping to the band dispersion :math:`\varepsilon_{ab}(k)`, looping over + Fourier-transforms the real-space hopping to the band dispersion :math:`\varepsilon_{12}(k)`, looping over lattice vectors to keep the memory footprint low. :param k_mesh: The k-points as an array of shape ``[3, nk]``. @@ -724,7 +724,7 @@ def _convham_2_orbs(self, k_mesh: np.ndarray) -> np.ndarray: def _convham_4_orbs(self, k_mesh: np.ndarray) -> np.ndarray: r""" - Fourier-transforms the real-space non-local interaction to :math:`V_{abcd}(q)`, looping over lattice vectors + Fourier-transforms the real-space non-local interaction to :math:`V_{1234}(q)`, looping over lattice vectors to keep the memory footprint low. :param k_mesh: The q-points as an array of shape ``[3, nk]``. @@ -775,8 +775,8 @@ def _check_interaction_swapping_symmetry(self, uq_local: np.ndarray, uq_nonlocal NOTE: The check for the non-local :math:`V^q` needs to be revised because a straight inversion of the first axis is not correct. - :param uq_local: The local interaction tensor :math:`U_{abcd}`. - :param uq_nonlocal: The non-local interaction tensor :math:`V_{abcd}(q)`. + :param uq_local: The local interaction tensor :math:`U_{1234}`. + :param uq_nonlocal: The non-local interaction tensor :math:`V_{1234}(q)`. :return: None. :raises AssertionError: If either swapping symmetry is violated. """ diff --git a/dgamore/interaction.py b/dgamore/interaction.py index acc695a7..8b8c5d16 100644 --- a/dgamore/interaction.py +++ b/dgamore/interaction.py @@ -1,17 +1,15 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Interaction tensors. :class:`LocalInteraction` wraps the momentum-independent (Hubbard/Kanamori) interaction -:math:`U_{abcd}`; :class:`Interaction` adds a momentum dimension for the non-local interaction :math:`V_{abcd}^q`. +:math:`U_{1234}`; :class:`Interaction` adds a momentum dimension for the non-local interaction :math:`V_{1234}^q`. Both provide the channel projections (density/magnetic/singlet/triplet) and the algebra used in the ladder equations. """ -from copy import deepcopy - import numpy as np from dgamore.n_point_base import IHaveMat, IHaveChannel, IAmNonLocal, SpinChannel @@ -19,14 +17,14 @@ class LocalInteraction(IHaveMat, IHaveChannel): r""" - Class for the local (momentum-independent) interaction :math:`U_{abcd}`. + Class for the local (momentum-independent) interaction :math:`U_{1234}`. """ def __init__(self, mat: np.ndarray, channel: SpinChannel = SpinChannel.NONE): r""" Initializes the local interaction tensor in the given spin channel. - :param mat: Interaction tensor :math:`U_{abcd}` with four orbital axes ``[o1, o2, o3, o4]``. + :param mat: Interaction tensor :math:`U_{1234}` with four orbital axes ``[o1, o2, o3, o4]``. :param channel: Spin channel the tensor is expressed in (see :class:`SpinChannel`); ``NONE`` for the bare, not-yet-projected interaction. """ @@ -55,21 +53,21 @@ def permute_orbitals(self, permutation: str = "abcd->abcd") -> "LocalInteraction raise ValueError("Invalid permutation.") if split[0] == split[1]: - return deepcopy(self) + return self.copy() return LocalInteraction(np.einsum(permutation, self.mat, optimize=True), self.channel) def as_channel(self, channel: SpinChannel) -> "LocalInteraction": r""" Projects the bare interaction onto a given spin channel. With the crossing-permuted tensor - :math:`\tilde U_{abcd} = U_{adcb}` the projections are + :math:`\tilde U_{1234} = U_{1432}` the projections are :math:`U_d = 2U - \tilde U`, :math:`U_m = -\tilde U`, :math:`U_s = U + \tilde U`, :math:`U_t = U - \tilde U`. :param channel: Target spin channel (density, magnetic, singlet or triplet). :return: A new :class:`LocalInteraction` in the requested channel. :raises ValueError: If the object is already in a (non-``NONE``) channel, or the target channel is unsupported. """ - copy = deepcopy(self) + copy = self.copy() if copy.channel == channel: return copy @@ -131,7 +129,7 @@ def sub(self, other) -> "LocalInteraction": def pow(self, power) -> "LocalInteraction": r""" Raises the interaction to an integer power via repeated orbital contraction - :math:`U^{(n)}_{abef} = U_{abcd} U^{(n-1)}_{dcef}`. + :math:`U^{(n)}_{1234} = \sum_{ab} U_{12ab} U^{(n-1)}_{ba34}`. :param power: Positive integer exponent (must be greater than zero). :return: A new :class:`LocalInteraction` equal to ``self`` contracted with itself ``power`` times. @@ -139,7 +137,7 @@ def pow(self, power) -> "LocalInteraction": """ if power <= 0: raise ValueError("Exponentiation of Interaction objects only supports positive powers greater than zero.") - result = deepcopy(self) + result = self.copy() for _ in range(1, power): result = LocalInteraction(result.times("abcd,dcef->abef", self), self.channel) return result @@ -193,7 +191,7 @@ def __pow__(self, power, modulo=None) -> "LocalInteraction": class Interaction(IAmNonLocal, LocalInteraction): r""" - Class for the non-local (momentum-dependent) interaction :math:`V_{abcd}^q`. + Class for the non-local (momentum-dependent) interaction :math:`V_{1234}^q`. """ def __init__( @@ -206,7 +204,7 @@ def __init__( r""" Initializes the non-local interaction tensor in the given spin channel and momentum layout. - :param mat: Interaction tensor :math:`V_{abcd}^q` with one momentum dimension and four orbital axes. + :param mat: Interaction tensor :math:`V_{1234}^q` with one momentum dimension and four orbital axes. :param channel: Spin channel the tensor is expressed in (see :class:`SpinChannel`). :param nq: Number of momenta per spatial direction ``(nqx, nqy, nqz)``. :param has_compressed_q_dimension: Whether the momentum is stored as a single compressed axis ``[q, ...]`` @@ -228,7 +226,7 @@ def permute_orbitals(self, permutation: str = "abcd->abcd") -> "Interaction": raise ValueError("Invalid permutation.") if split[0] == split[1]: - return deepcopy(self) + return self.copy() permutation = f"...{split[0]}->...{split[1]}" return Interaction( @@ -245,7 +243,7 @@ def as_channel(self, channel: SpinChannel) -> "Interaction": :return: A new :class:`Interaction` in the requested channel. :raises ValueError: If the object is already in a (non-``NONE``) channel, or the target channel is unsupported. """ - copy = deepcopy(self) + copy = self.copy() if copy.channel == channel: return copy @@ -316,7 +314,7 @@ def sub(self, other) -> "Interaction": def pow(self, power) -> "Interaction": r""" Raises the interaction to an integer power via repeated momentum-diagonal orbital contraction - :math:`V^{(n);q}_{abef} = V^{q}_{abcd} V^{(n-1);q}_{dcef}`. + :math:`V^{(n);q}_{1234} = \sum_{ab} V^{q}_{12ab} V^{(n-1);q}_{ba34}`. :param power: Positive integer exponent (must be greater than zero). :return: A new :class:`Interaction` in the same momentum-compression state as ``self``. @@ -325,7 +323,7 @@ def pow(self, power) -> "Interaction": if power <= 0: raise ValueError("Exponentiation of Interaction objects only supports positive powers greater than zero.") is_self_compressed = self.has_compressed_q_dimension - result = deepcopy(self).compress_q_dimension() + result = self.copy().compress_q_dimension() for _ in range(1, power): result = Interaction(result.times("qabcd,qdcef->qabef", self), self.channel, self.nq, True) return result if is_self_compressed else result.decompress_q_dimension() diff --git a/dgamore/lambda_correction.py b/dgamore/lambda_correction.py index f065e870..c6fa5e79 100644 --- a/dgamore/lambda_correction.py +++ b/dgamore/lambda_correction.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Moriya :math:`\lambda`-correction of the physical susceptibility. The non-local susceptibility is shifted by a diff --git a/dgamore/local_four_point.py b/dgamore/local_four_point.py index 7e774031..14041bf1 100644 --- a/dgamore/local_four_point.py +++ b/dgamore/local_four_point.py @@ -1,19 +1,17 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Local (momentum-independent) four-point objects. :class:`LocalFourPoint` wraps a single array carrying four orbital indices and a variable number of bosonic/fermionic Matsubara axes, e.g. the generalized susceptibility -:math:`\chi_{abcd}^{\omega\nu\nu'}`, the vertex :math:`F`, the irreducible vertex :math:`\Gamma`, and the +:math:`\chi_{1234}^{\omega\nu\nu'}`, the vertex :math:`F`, the irreducible vertex :math:`\Gamma`, and the physical susceptibility :math:`\chi`. It adds channel-/notation-aware arithmetic (``+``, ``-``, ``*``, ``@``, inversion, integer powers), orbital and frequency contractions, and conversions to/from the compound-index matrix layout used for inversion and matrix products. Notation mirrors the thesis (Chapters 3 & 4). """ -from copy import deepcopy - import numpy as np from dgamore.interaction import LocalInteraction, Interaction @@ -82,7 +80,7 @@ def __rsub__(self, other): """ Reflected operator form of :meth:`sub` (``B - A``), returning ``-(A - B)``. See :meth:`sub`. """ - return -self.sub(other) + return self.sub(other).scale(-1.0) def __mul__(self, other): """ @@ -116,32 +114,41 @@ def __invert__(self): def __pow__(self, power, modulo=None): """ - Operator form of :meth:`pow` (``A ** n``), supplying the matching identity. See :meth:`pow`. + Operator form of :meth:`pow` (``A ** n``). See :meth:`pow`. """ - return self.pow(power, LocalFourPoint.identity_like(self)) + return self.pow(power) - def pow(self, power: int, identity): + def pow(self, power: int, identity=None): r""" - Raises the object to an integer power using exponentiation by squaring (in compound-index matrix space). - For :math:`n < 0` the inverse is exponentiated, i.e. :math:`A^{-n} = (A^{-1})^{n}`. + Raises the object to an integer power using exponentiation by squaring (in compound-index matrix space of + the object's :attr:`~dgamore.n_point_base.IHaveChannel.frequency_notation`). For :math:`n < 0` the inverse + (see :meth:`invert`) is exponentiated, i.e. :math:`A^{-n} = (A^{-1})^{n}`. :param power: Integer exponent :math:`n`. :param identity: Identity object matching ``self`` (see :meth:`identity_like`), returned for :math:`n = 0`. - :return: The exponentiated :class:`LocalFourPoint`. - :raises ValueError: If ``power`` is not an integer. + Built via :meth:`identity_like` (carrying the frequency notation of ``self``) if omitted. + :return: The exponentiated object. + :raises ValueError: If ``power`` is not an integer or ``identity`` does not share the frequency notation + of ``self``. """ if not isinstance(power, int): raise ValueError("Only integer powers are supported.") - if power == 0: - return identity + if identity is not None and identity.frequency_notation != self.frequency_notation: + raise ValueError("Identity must be in the same frequency notation as the object.") + if power == 1: return self if power < 0: return self.invert() ** abs(power) + if identity is None: + identity = type(self).identity_like(self) + if power == 0: + return identity + result = identity - base = deepcopy(self) + base = self.copy() # Exponentiation by squaring while power > 0: @@ -219,7 +226,7 @@ def contract_legs(self, beta: float): r""" Contracts the outer legs of a four-point vertex: sums over both fermionic frequency axes (with the :math:`1/\beta^2` prefactor) and traces the inner two orbitals (``abcd->ad``). Used e.g. to obtain the - physical susceptibility :math:`\chi_{ad}^{\omega}` from a generalized susceptibility. + physical susceptibility :math:`\chi_{14}^{\omega}` from a generalized susceptibility. :param beta: Inverse temperature :math:`\beta`. :return: A new :class:`LocalFourPoint` with two orbital indices and no fermionic frequency axes. @@ -231,7 +238,7 @@ def contract_legs(self, beta: float): def to_compound_indices(self) -> "LocalFourPoint": r""" - Converts the indices of the LocalFourPoint object :math:`F^{wvv'}_{lmm'l'}` to compound indices :math:`F^{w}_{c_1, c_2}` + Converts the indices of the LocalFourPoint object :math:`F^{\omega\nu\nu'}_{1234}` to compound indices :math:`F^{\omega}_{c_1, c_2}` by transposing the object to [w, o1, o2, v, o4, o3, v'] (if the object has any fermionic frequency dimension, otherwise the compound indices are built from orbital dimensions only) and grouping {o1, o2, v} and {o4, o3, v'} to the new compound index. Always returns the object in the same niw range as the original object. @@ -286,7 +293,7 @@ def _to_compound_indices_pp(self) -> "LocalFourPoint": :return: ``self`` in compound-index layout. """ - if len(self.current_shape) == 3: # [w,x1,x2] + if len(self.current_shape) == 2 + self.num_wn_dimensions: # [w, x1, x2] or [x1, x2] return self return self.permute_orbitals("abcd->acbd", copy=False)._to_compound_indices_ph() @@ -370,7 +377,7 @@ def invert(self, copy: bool = True): """ if copy: - return deepcopy(self).invert(copy=False) + return self.copy().invert(copy=False) self.to_half_niw_range().to_compound_indices() self.mat = np.linalg.inv(self.mat) @@ -387,18 +394,31 @@ def matmul(self, other, left_hand_side: bool = True) -> "LocalFourPoint": :param other: The right/left operand, a :class:`LocalFourPoint` or :class:`LocalInteraction`. :param left_hand_side: If True, compute ``self @ other``; if False, compute ``other @ self``. - :return: A new :class:`LocalFourPoint` in the half bosonic frequency range, carrying the non-NONE channel. - :raises ValueError: If ``other`` is neither a :class:`LocalFourPoint` nor a :class:`LocalInteraction`. + :return: A new :class:`LocalFourPoint` in the half bosonic frequency range, carrying the non-NONE channel + and the :attr:`~dgamore.n_point_base.IHaveChannel.frequency_notation` of ``self``. All branches + contract in the compound space of that notation (ph: rows {1,2,v}, cols {4,3,v'}; pp: rows {1,3,v}, + cols {4,2,v'}; see :meth:`to_compound_indices`). + :raises ValueError: If ``other`` is neither a :class:`LocalFourPoint` nor a :class:`LocalInteraction`, or + if the operands' frequency notations differ. """ if not isinstance(other, (LocalFourPoint, LocalInteraction)): raise ValueError(f"Multiplication {type(self)} @ {type(other)} not supported.") + if isinstance(other, LocalFourPoint) and other.frequency_notation != self.frequency_notation: + raise ValueError("Cannot multiply two objects with different frequency notations.") + if isinstance(other, LocalInteraction): - einsum_str = { - 0: "abijw,jief->abefw" if left_hand_side else "abij,jiefw->abefw", - 1: "abijwv,jief->abefwv" if left_hand_side else "abij,jiefwv->abefwv", - 2: "abijwvp,jief->abefwvp" if left_hand_side else "abij,jiefwvp->abefwvp", - }.get(self.num_vn_dimensions) + left_orbs, right_orbs, final_orbs = ( + ("abij", "jief", "abef") + if self.frequency_notation == FrequencyNotation.PH + else ("afce", "ebfd", "abcd") + ) + suffix = {0: "w", 1: "wv", 2: "wvp"}.get(self.num_vn_dimensions) + einsum_str = ( + f"{left_orbs}{suffix},{right_orbs}->{final_orbs}{suffix}" + if left_hand_side + else f"{left_orbs},{right_orbs}{suffix}->{final_orbs}{suffix}" + ) return LocalFourPoint( ( np.einsum(einsum_str, self.mat, other.mat, optimize=True) @@ -424,10 +444,15 @@ def matmul(self, other, left_hand_side: bool = True) -> "LocalFourPoint": suffix_other, suffix_result, suffix_self = self._get_frequency_suffixes_for_matmul(other, left_hand_side) + left_orbs, right_orbs, final_orbs = ( + ("abcd", "dcef", "abef") + if self.frequency_notation == FrequencyNotation.PH + else ("afce", "ebfd", "abcd") + ) einsum_str = ( - f"abcd{suffix_self},dcef{suffix_other}->abef{suffix_result}" + f"{left_orbs}{suffix_self},{right_orbs}{suffix_other}->{final_orbs}{suffix_result}" if left_hand_side - else f"abcd{suffix_other},dcef{suffix_self}->abef{suffix_result}" + else f"{left_orbs}{suffix_other},{right_orbs}{suffix_self}->{final_orbs}{suffix_result}" ) return LocalFourPoint( @@ -441,6 +466,7 @@ def matmul(self, other, left_hand_side: bool = True) -> "LocalFourPoint": max(self.num_vn_dimensions, other.num_vn_dimensions), self.full_niw_range, self.full_niv_range, + self.frequency_notation, ) self_full_niw_range = self.full_niw_range @@ -472,13 +498,14 @@ def matmul(self, other, left_hand_side: bool = True) -> "LocalFourPoint": max(self.num_vn_dimensions, other.num_vn_dimensions), full_niw_range=False, full_niv_range=self.full_niv_range, + frequency_notation=self.frequency_notation, ).to_full_indices(shape) def mul(self, other): r""" Multiplies the object by a scalar/array (element-wise) or by another :class:`LocalFourPoint`. Note this is **not** a matrix product (see :meth:`matmul`): for two four-point operands, each with a single fermionic - frequency, it forms :math:`A_{abcd}^{\omega\nu} \, B_{dcef}^{\omega\nu'} = C_{abef}^{\omega\nu\nu'}`, + frequency, it forms :math:`\sum_{ab} A_{12ab}^{\omega\nu} \, B_{ba34}^{\omega\nu'} = C_{1234}^{\omega\nu\nu'}`, contracting the inner orbitals while keeping both fermionic frequencies as separate axes. :param other: A number, numpy array, or :class:`LocalFourPoint`. @@ -490,7 +517,7 @@ def mul(self, other): raise ValueError("Multiplication only supported with numbers, numpy arrays or LocalFourPoint objects.") if not isinstance(other, LocalFourPoint): - copy = deepcopy(self) + copy = self.copy() copy.mat *= other return copy @@ -664,7 +691,7 @@ def permute_orbitals(self, permutation: str = "abcd->abcd", copy: bool = True) - raise ValueError("Invalid permutation.") if copy: - return deepcopy(self).permute_orbitals(permutation, copy=False) + return self.copy().permute_orbitals(permutation, copy=False) permutation = f"{split[0]}...->{split[1]}..." self.mat = np.einsum(permutation, self.mat, optimize=True) @@ -695,7 +722,7 @@ def is_orbitally_symmetrized(self, orbitals: list | np.ndarray) -> bool: def symmetrize_v_vp(self): r""" Symmetrizes the vertex with respect to :math:`(\nu, \nu')` via time-reversal symmetry: - :math:`G_{abcd}(\nu, \nu', \omega) = G_{dcba}(\nu', \nu, \omega)`. Valid for TR-symmetric (real-hopping, + :math:`G_{1234}(\nu, \nu', \omega) = G_{4321}(\nu', \nu, \omega)`. Valid for TR-symmetric (real-hopping, paramagnetic) systems with SU(2) symmetry. Mutates ``self`` in place. :return: ``self``, symmetrized in :math:`(\nu, \nu')`. @@ -724,7 +751,7 @@ def change_frequency_notation_ph_to_pp_w0(self): if self.num_wn_dimensions != 1 or self.num_vn_dimensions not in (1, 2): raise ValueError("Object must have 1 bosonic and 1 or 2 fermionic frequency dimensions.") - copy = deepcopy(self) + copy = self.copy() if copy.frequency_notation == FrequencyNotation.PP: return copy @@ -749,7 +776,7 @@ def create_wn_dimension(self): if self.num_wn_dimensions != 0: raise ValueError("Object already has bosonic frequency dimensions.") - copy = deepcopy(self) + copy = self.copy() # insert bosonic axis immediately before the last vn axes copy.mat = np.expand_dims(copy.mat, axis=-(copy.num_vn_dimensions + 1)) copy._num_wn_dimensions = 1 @@ -786,7 +813,7 @@ def pad_with_u(self, u: LocalInteraction, niv_pad: int): :return: A new :class:`LocalFourPoint` padded to ``niv_pad``. """ if niv_pad <= self.niv: - return deepcopy(self) + return self.copy() copy = self._clone_without_mat() # Allocate the padded array once and broadcast-fill the shell with ``u`` instead of materializing a full diff --git a/dgamore/local_n_point.py b/dgamore/local_n_point.py index 016b8be6..420fb078 100644 --- a/dgamore/local_n_point.py +++ b/dgamore/local_n_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Base class for all local (momentum-independent) N-point quantities. :class:`LocalNPoint` adds the orbital and @@ -12,7 +12,6 @@ import itertools import os -from copy import deepcopy import numpy as np @@ -341,7 +340,7 @@ def to_full_niw_range(self): def to_half_niw_range(self): r""" Converts the object to the half bosonic frequency range by taking - :math:`F^{\omega\nu\nu'}_{abcd}\to F^{\omega\geq0;\nu\nu'}_{abcd}`. Returns the original object. + :math:`F^{\omega\nu\nu'}_{1234}\to F^{\omega\geq0;\nu\nu'}_{1234}`. Returns the original object. :return: ``self`` over the half bosonic range (a no-op if there is no bosonic axis or it is already half). """ @@ -364,7 +363,7 @@ def to_negative_niw_range(self): Returns a **new** object holding the negative bosonic frequency block :math:`\omega = 0, -1, \ldots, -niw` (``niw + 1`` entries, :math:`\omega = 0` included for consistency with :meth:`to_half_niw_range`), derived from a half (positive) niw-range object via the time-reversal symmetry - :math:`F^{-\omega,-\nu,-\nu'}_{abcd} = (F^{\omega\nu\nu'}_{abcd})^{*}`. The bosonic axis order is kept so that + :math:`F^{-\omega,-\nu,-\nu'}_{1234} = (F^{\omega\nu\nu'}_{1234})^{*}`. The bosonic axis order is kept so that index ``i`` corresponds to :math:`\omega = -i` (index 0 is :math:`\omega = 0`); only the fermionic axes are flipped and the whole array is conjugated. This is the negative-frequency counterpart of :meth:`to_full_niw_range`, and it is its own inverse (applying it twice returns the original object). @@ -397,7 +396,7 @@ def to_negative_niw_range(self): def to_half_niv_range(self): r""" Converts the object to the half fermionic frequency range by taking - :math:`F^{\omega\nu\nu'}_{abcd}\to F^{\omega;\nu\geq0,\nu'\geq0}_{abcd}`. Returns the original object. + :math:`F^{\omega\nu\nu'}_{1234}\to F^{\omega;\nu\geq0,\nu'\geq0}_{1234}`. Returns the original object. :return: ``self`` over the half fermionic range (a no-op if there is no fermionic axis or it is already half). """ @@ -435,7 +434,7 @@ def flip_frequency_axis(self, axis: tuple | int, copy: bool = True): raise ValueError(f"Invalid axis {axis}. Possible axes are {axis_possible}.") if copy: - return deepcopy(self).flip_frequency_axis(axis, copy=False) + return self.copy().flip_frequency_axis(axis, copy=False) self.mat = np.flip(self.mat, axis=axis) return self @@ -452,7 +451,7 @@ def swap_fermionic_frequency_axes(self, copy: bool = True): raise ValueError("Cannot swap axes if there are less than two fermionic frequency dimensions.") if copy: - return deepcopy(self).swap_fermionic_frequency_axes(copy=False) + return self.copy().swap_fermionic_frequency_axes(copy=False) self.mat = np.swapaxes(self.mat, -1, -2) return self diff --git a/dgamore/local_sde.py b/dgamore/local_sde.py index e1c16089..f9c3d68b 100644 --- a/dgamore/local_sde.py +++ b/dgamore/local_sde.py @@ -1,14 +1,14 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Local Schwinger-Dyson step. Given the two-particle DMFT Green's functions and the bare interaction, the functions -here build the local vertex hierarchy per spin channel — the generalized susceptibility :math:`\chi_{r}`, the +here build the local vertex hierarchy per spin channel - the generalized susceptibility :math:`\chi_{r}`, the irreducible vertex :math:`\Gamma_{r}` (with the Kitatani shell asymptotics), the auxiliary susceptibility :math:`\chi^{*}_{r}`, the three-leg vertex :math:`\gamma_{r}` (``vrg``), the full vertex :math:`F_{r}`, and the -physical susceptibility — and recompute the local self-energy via the Schwinger-Dyson equation as a sanity check +physical susceptibility - and recompute the local self-energy via the Schwinger-Dyson equation as a sanity check against the DMFT input. Equation numbers refer to the author's master's thesis (Chapter 3). A second set of functions implements the alternative ab-initio DGA formulation. """ @@ -28,8 +28,8 @@ def create_generalized_chi(g2: LocalFourPoint, g_dmft: GreensFunction) -> LocalFourPoint: r""" Returns the generalized susceptibility, see also Eq. (3.41) in my master's thesis, - :math:`\chi_{r;abcd}^{\omega\nu\nu'} = \beta (G_{r;abcd}^{(2);\omega\nu\nu'} - 2 \delta_{r,\mathrm{dens}} - \delta_{\omega 0} G_{ab}^{\nu} G_{cd}^{\nu'})`. The disconnected term is subtracted only in the density + :math:`\chi_{r;1234}^{\omega\nu\nu'} = \beta (G_{r;1234}^{(2);\omega\nu\nu'} - 2 \delta_{r,\mathrm{dens}} + \delta_{\omega 0} G_{12}^{\nu} G_{34}^{\nu'})`. The disconnected term is subtracted only in the density (ph) channel at :math:`\omega = 0`. :param g2: Two-particle (DMFT) Green's function :math:`G^{(2)}_{r}` as a :class:`LocalFourPoint`. @@ -49,15 +49,15 @@ def create_generalized_chi(g2: LocalFourPoint, g_dmft: GreensFunction) -> LocalF def create_gamma_r(gchi_r: LocalFourPoint, gchi0_inv: LocalFourPoint, beta: float) -> LocalFourPoint: r""" Returns the ph-irreducible vertex - :math:`\Gamma_{r;abcd}^{\omega\nu\nu'} = \beta^2 [(\chi_{r;abcd}^{\omega\nu\nu'})^{-1} - - (\delta_{\nu\nu'}\chi_{0;abcd}^{\omega\nu\nu'})^{-1}]`. + :math:`\Gamma_{r;1234}^{\omega\nu\nu'} = \beta^2 [(\chi_{r;1234}^{\omega\nu\nu'})^{-1} - + (\delta_{\nu\nu'}\chi_{0;1234}^{\omega\nu})^{-1}]`. :param gchi_r: The generalized susceptibility :math:`\chi_{r}`. :param gchi0_inv: The inverse bare bubble :math:`\chi_0^{-1}` (diagonal in :math:`\nu\nu'`). :param beta: Inverse temperature :math:`\beta`. :return: The irreducible vertex :math:`\Gamma_{r}` as a :class:`LocalFourPoint`. """ - return beta**2 * (gchi_r.invert() - gchi0_inv) + return (gchi_r.invert() - gchi0_inv).scale(beta**2) def create_gamma_r_with_shell_correction( @@ -76,21 +76,21 @@ def create_gamma_r_with_shell_correction( """ chi_tilde_shell = (gchi0.invert() + 1.0 / config.sys.beta**2 * u_loc.as_channel(gchi_r.channel)).invert() chi_tilde_core_inv = chi_tilde_shell.cut_niv(config.box.niv_core).invert() - return config.sys.beta**2 * (gchi_r.invert() - chi_tilde_core_inv) + u_loc.as_channel(gchi_r.channel) + return (gchi_r.invert() - chi_tilde_core_inv).scale(config.sys.beta**2) + u_loc.as_channel(gchi_r.channel) def create_auxiliary_chi(gamma_r: LocalFourPoint, gchi0_inv: LocalFourPoint, u_loc: LocalInteraction) -> LocalFourPoint: r""" Returns the auxiliary susceptibility, see Eq. (3.60) in my master's thesis, - :math:`\chi^{*;\omega\nu\nu'}_{r;abcd} = ((\chi_{0;abcd}^{\omega\nu})^{-1} + - (\Gamma_{r;abcd}^{\omega\nu\nu'} - U_{r;abcd})/\beta^2)^{-1}`. + :math:`\chi^{*;\omega\nu\nu'}_{r;1234} = ((\chi_{0;1234}^{\omega\nu})^{-1} + + (\Gamma_{r;1234}^{\omega\nu\nu'} - U_{r;1234})/\beta^2)^{-1}`. :param gamma_r: The irreducible vertex :math:`\Gamma_{r}`. :param gchi0_inv: The inverse bare bubble :math:`\chi_0^{-1}` (core box). :param u_loc: The bare local interaction :math:`U`. :return: The auxiliary susceptibility :math:`\chi^{*}_{r}` as a :class:`LocalFourPoint`. """ - return (gchi0_inv + (gamma_r - u_loc.as_channel(gamma_r.channel)) / config.sys.beta**2).invert() + return (gchi0_inv + (gamma_r - u_loc.as_channel(gamma_r.channel)).scale(1.0 / config.sys.beta**2)).invert() def create_generalized_chi_with_shell_correction( @@ -106,8 +106,8 @@ def create_generalized_chi_with_shell_correction( :param u_loc: The bare local interaction :math:`U`. :return: The shell-corrected physical susceptibility :math:`\chi_{r}^{\omega}` as a :class:`LocalFourPoint`. """ - gchi0_full_sum = 1.0 / config.sys.beta * gchi0.sum_over_all_vn(config.sys.beta) - gchi0_core_sum = 1.0 / config.sys.beta * gchi0.cut_niv(config.box.niv_core).sum_over_all_vn(config.sys.beta) + gchi0_full_sum = gchi0.sum_over_all_vn(config.sys.beta).scale(1.0 / config.sys.beta) + gchi0_core_sum = gchi0.cut_niv(config.box.niv_core).sum_over_all_vn(config.sys.beta).scale(1.0 / config.sys.beta) return ((gchi_aux_sum + gchi0_full_sum - gchi0_core_sum).invert() + u_loc.as_channel(gchi_aux_sum.channel)).invert() @@ -130,28 +130,28 @@ def create_full_vertex_from_gamma(gamma_r, gchi0, u_loc): def create_full_vertex(gchi_r: LocalFourPoint, gchi0_inv: LocalFourPoint) -> LocalFourPoint: r""" Returns the local full vertex in the ``niv_core`` region, see Eq. (3.58) in my master's thesis, - :math:`F_{r;abcd}^{\omega\nu\nu'} = \beta^2 (\chi_{0;abcd}^{-1} - \chi_{0;abef}^{-1} \chi_{r;fehg} - \chi_{0;ghcd}^{-1})`. + :math:`F_{r;1234}^{\omega\nu\nu'} = \beta^2 ((\chi_{0;1234}^{\omega\nu\nu'})^{-1} - \sum_{abcd} (\chi_{0;12ab}^{\omega\nu})^{-1} + \chi_{r;bacd}^{\omega\nu\nu'} (\chi_{0;dc34}^{\omega\nu'})^{-1})`. :param gchi_r: The generalized susceptibility :math:`\chi_{r}`. :param gchi0_inv: The inverse bare bubble :math:`\chi_0^{-1}`. :return: The full vertex :math:`F_{r}` as a :class:`LocalFourPoint`. """ - return config.sys.beta**2 * (gchi0_inv - gchi0_inv @ gchi_r @ gchi0_inv) + return (gchi0_inv - gchi0_inv @ gchi_r @ gchi0_inv).scale(config.sys.beta**2) def create_vrg(gchi_aux: LocalFourPoint, gchi0_inv: LocalFourPoint) -> LocalFourPoint: r""" Returns the three-leg vertex, see Eq. (3.63) in my master's thesis, - :math:`\gamma_{r;abcd}^{\omega\nu} = \beta (\chi^{\omega\nu\nu}_{0;ablm})^{-1} (\sum_{\nu'} - \chi^{*;\omega\nu\nu'}_{r;mlcd})`. + :math:`\gamma_{r;1234}^{\omega\nu} = \beta \sum_{ab} \sum_{\nu'} (\chi^{\omega\nu}_{0;12ab})^{-1} + \chi^{*;\omega\nu\nu'}_{r;ba34}`. :param gchi_aux: The auxiliary susceptibility :math:`\chi^{*}_{r}`. :param gchi0_inv: The inverse bare bubble :math:`\chi_0^{-1}` (core box). :return: The three-leg vertex :math:`\gamma_{r}` (``vrg``) as a :class:`LocalFourPoint`. """ gchi_aux_sum = gchi_aux.sum_over_vn(config.sys.beta, axis=(-1,)) - return config.sys.beta * (gchi0_inv @ gchi_aux_sum) + return (gchi0_inv @ gchi_aux_sum).scale(config.sys.beta) def create_vertex_functions( @@ -212,18 +212,18 @@ def create_vertex_functions( def get_local_hartree_fock(u_loc: LocalInteraction, occ: np.ndarray) -> np.ndarray: r""" Returns the local Hartree-Fock (static, frequency-independent) self-energy - :math:`\Sigma^{HF}_{ab}` from the bare interaction and the local occupation, i.e. the density-channel + :math:`\Sigma^{HF}_{12}` from the bare interaction and the local occupation, i.e. the density-channel interaction contracted with the occupation, see Eq. (3.55) in my master's thesis. - The interaction tensor is stored with the inter-orbital density :math:`U'` at :math:`U_{abab}` (the convention + The interaction tensor is stored with the inter-orbital density :math:`U'` at :math:`U_{1212}` (the convention of :meth:`Hamiltonian.kanamori_interaction_dp` and the w2dynamics ``umatrix`` files), whereas the density-channel - projection contracted as ``"abcd,dc->ab"`` picks up :math:`U_{aabb}`. The middle two orbital indices are + projection contracted as ``"abcd,dc->ab"`` picks up :math:`U_{1122}`. The middle two orbital indices are therefore swapped (``"abcd->acbd"``) before the projection, so that the Hartree term uses :math:`U'` while the - Fock term still uses :math:`U_{adcb}`. This only affects multi-orbital systems with off-diagonal interactions; + Fock term still uses :math:`U_{1432}`. This only affects multi-orbital systems with off-diagonal interactions; single-orbital and purely orbital-diagonal interactions are unchanged. :param u_loc: The bare local interaction :math:`U`. - :param occ: The local occupation matrix :math:`n_{ab}`, shape ``[n_bands, n_bands]``. + :param occ: The local occupation matrix :math:`n_{12}`, shape ``[n_bands, n_bands]``. :return: The Hartree-Fock self-energy, shape ``[n_bands, n_bands]``. """ return u_loc.permute_orbitals("abcd->acbd").as_channel(SpinChannel.DENS).times("abcd,dc->ab", occ) @@ -380,20 +380,20 @@ def perform_local_schwinger_dyson_abinitio_dga( # F_r = -beta^2 * [chi0^(-1) - chi0^(-1) chi_r chi0^(-1)] # gamma_r is NOT the irreducible vertex in channel r but rather the three-point vertex from AbinitioDGA gchi0_inv_core = gchi0_core.invert() - f_dens_loc = -config.sys.beta**2 * (gchi0_inv_core - gchi0_inv_core @ gchi_dens_loc @ gchi0_inv_core) + f_dens_loc = (gchi0_inv_core - gchi0_inv_core @ gchi_dens_loc @ gchi0_inv_core).scale(-config.sys.beta**2) logger.info("Local full vertex F^wvv' (dens) calculated.") - f_magn_loc = -config.sys.beta**2 * (gchi0_inv_core - gchi0_inv_core @ gchi_magn_loc @ gchi0_inv_core) + f_magn_loc = (gchi0_inv_core - gchi0_inv_core @ gchi_magn_loc @ gchi0_inv_core).scale(-config.sys.beta**2) logger.info("Local full vertex F^wvv' (magn) calculated.") del gchi0_inv_core # f_dens_loc_with_asympt = create_asympt_f(gchi_dens_loc, gchi_magn_loc, gchi_ud_pp_loc_sum, u_loc) # in most equations we need 1 + gamma_r so we add it here - gamma_dens_loc = 1.0 / config.sys.beta * (gchi0_core @ f_dens_loc).sum_over_vn(config.sys.beta, axis=(-2,)) + gamma_dens_loc = (gchi0_core @ f_dens_loc).sum_over_vn(config.sys.beta, axis=(-2,)).scale(1.0 / config.sys.beta) one_plus_gamma_dens_loc = LocalFourPoint.identity_like(gamma_dens_loc) + gamma_dens_loc logger.info("Local three-leg vertex gamma^wv (dens) calculated.") - gamma_magn_loc = 1.0 / config.sys.beta * (gchi0_core @ f_magn_loc).sum_over_vn(config.sys.beta, axis=(-2,)) + gamma_magn_loc = (gchi0_core @ f_magn_loc).sum_over_vn(config.sys.beta, axis=(-2,)).scale(1.0 / config.sys.beta) one_plus_gamma_magn_loc = LocalFourPoint.identity_like(gamma_magn_loc) + gamma_magn_loc logger.info("Local three-leg vertex gamma^wv (magn) calculated.") del gchi0_core, gamma_magn_loc diff --git a/dgamore/local_two_point.py b/dgamore/local_two_point.py index b68a0d9a..f8cd7085 100644 --- a/dgamore/local_two_point.py +++ b/dgamore/local_two_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Base class for the local (momentum-independent) two-point quantities. :class:`LocalTwoPoint` wraps a single array @@ -12,8 +12,6 @@ single momentum dimension on top (just as :class:`FourPoint` extends :class:`LocalFourPoint`). """ -from copy import deepcopy - import numpy as np from dgamore.local_n_point import LocalNPoint @@ -69,7 +67,7 @@ def permute_orbitals(self, permutation: str = "ab->ab") -> "LocalTwoPoint": if split[0] == split[1]: return self - copy = deepcopy(self) + copy = self.copy() copy.mat = np.einsum(f"{split[0]}...->{split[1]}...", copy.mat, optimize=True) return copy diff --git a/dgamore/matsubara_frequencies.py b/dgamore/matsubara_frequencies.py index 3f703948..edfbafdc 100644 --- a/dgamore/matsubara_frequencies.py +++ b/dgamore/matsubara_frequencies.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Helpers for Matsubara frequency arithmetic. :class:`MFHelper` builds the integer index grids and the diff --git a/dgamore/max_ent.py b/dgamore/max_ent.py index ba5896bb..a2472c1a 100644 --- a/dgamore/max_ent.py +++ b/dgamore/max_ent.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Analytic continuation of imaginary-frequency quantities to the real axis via the maximum-entropy method. This @@ -11,8 +11,11 @@ irreducible BZ. """ +import contextlib import gc +import io import os +import warnings import numpy as np from mpi4py import MPI @@ -85,9 +88,7 @@ def perform_maxent_giwk(giwk: GreensFunction, name: str, comm: MPI.Comm): irrq_list = config.lattice.k_grid.get_irrq_list() - mpi_dist = MpiDistributor( - ntasks=len(irrq_list), comm=comm, name="Maxent_G", output_path=config.output.output_path - ) + mpi_dist = MpiDistributor(ntasks=len(irrq_list), comm=comm, name="Maxent_G", output_path=config.output.output_path) giwk_maxent = giwk_maxent.reduce_q(irrq_list) logger.info("Scattering Green's function in the IBZ to all ranks.") @@ -108,17 +109,33 @@ def perform_maxent_giwk(giwk: GreensFunction, name: str, comm: MPI.Comm): for band in range(config.sys.n_bands): logger.info(f"Processing analytic continuation of band {band+1}.") for k in range(giwk_maxent.mat.shape[0]): + # Capture the vendored solver's stdout so its print() diagnostics go through the logger instead of + # leaking to the output; re-logged (prefixed) below whether the continuation succeeds or fails. + captured_output = io.StringIO() try: - probl_maxent = AnalyticContinuationProblem( - im_axis=wn, re_axis=w, im_data=giwk_maxent[k, band, band], beta=config.sys.beta - ) - result = probl_maxent.solve(model=model, stdev=stdev)[0] - spectral_function[k, band] = result.A_opt.astype(np.float32) + with warnings.catch_warnings(), contextlib.redirect_stdout(captured_output): + # Escalate numpy/scipy RuntimeWarnings (divide/invalid/overflow) to exceptions so a numerically + # broken continuation falls through to the A(k, w) = 0 fallback below (its own errors are caught too). + warnings.simplefilter("error", RuntimeWarning) + probl_maxent = AnalyticContinuationProblem( + im_axis=wn, re_axis=w, im_data=giwk_maxent[k, band, band], beta=config.sys.beta + ) + result = probl_maxent.solve(model=model, stdev=stdev)[0] + spectral_function[k, band] = result.A_opt.astype(np.float32) del probl_maxent, result gc.collect() except Exception: + kpt = tuple(int(c) for c in irrq_list[mpi_dist.my_tasks[k]]) + logger.info( + f"Failed to determine analytic continuation of k={kpt} (band {band + 1}), " + f"setting A(k={kpt}, w) = 0.0." + ) spectral_function[k, band] = 0.0 + finally: + for message in captured_output.getvalue().splitlines(): + if message.strip(): + logger.info(f"ana_cont: {message}") mpi_dist.comm.barrier() logger.info(f"Completed analytic continuation of band {band+1}.") spectral_function = mpi_dist.gather(spectral_function) diff --git a/dgamore/memory_estimator.py b/dgamore/memory_estimator.py index c34d48fb..790b8b17 100644 --- a/dgamore/memory_estimator.py +++ b/dgamore/memory_estimator.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Pure, side-effect-free estimator of the peak host-memory of the memory-sensitive DGAmore operations. Each @@ -13,7 +13,14 @@ All heavy quantities are backed by a single :data:`~dgamore.n_point_base.DTYPE` array, and q-points are distributed across MPI ranks, so per-rank arrays scale with the per-rank q-count rather than the total. Only the dominant large -arrays of each branch are modeled; a single global ``OVERHEAD_FACTOR`` accounts for the un-modeled transients. +arrays of each branch are modeled; a single global ``OVERHEAD_FACTOR`` accounts for the un-modeled transients (known +un-modeled costs: the mixing history of ``apply_mixing_strategy`` and the pp pairing-vertex assembly in +``eliashberg_solver.solve``, both dominated by the modeled branch peaks). + +Each branch carries its **own** persistent per-rank ``baseline`` (the full-grid two-point objects resident at that +branch's peak) and the portion of it (``giwk_shareable``) that ``config.memory.use_shared_memory_giwk`` deduplicates +to a single copy per node; the node-total assembly lives in the driver +(:func:`dgamore.DGAmore.autodetect_memory_settings`). """ from dataclasses import dataclass @@ -37,24 +44,49 @@ # second accumulated term, holding ~3 full blocks live at once. FQ_MATMUL_FACTOR: int = 3 +# The fast (FFT) SDE holds the mapped full-BZ niw-half kernel plus its half-niv copy in flight through the conj / +# distributed-FFT round trip (in- and output buffers of half the kernel each), so ~2 kernel-sized blocks are live. +SDE_FFT_KERNEL_FACTOR: int = 2 + +# ``numpy.fft.ifftn`` on the complex64 full-grid bubble buffer transiently allocates ~2x the buffer beyond it +# (the returned array plus internal work arrays; measured 2026-07-03 on numpy 2.x, dtype preserved), so the per-iw +# FFT moment holds the multiply buffer plus this transient. +CHI0Q_IFFTN_TRANSIENT_FACTOR: int = 2 + +# The in-memory Eliashberg solver holds the direct vertex, its momentum-/frequency-flipped copy and one freshly +# materialized matmul-layout copy at the layout-build peak (each einsum-layout source is freed right after its +# matmul-layout copy exists, so the peak is 3 vertices, the residency 2). +LANCZOS_VERTEX_FACTOR: int = 3 + +# Gap-vector-sized matvec temporaries live beyond the ARPACK Lanczos basis (chi0*gap, its flipped copy, the +# contracted output) in both solver variants. +ARPACK_EXTRA_VECTORS: int = 3 + @dataclass(frozen=True) class BranchPeak: """ - Per-rank transient peak bytes of one memory-sensitive branch, split by how the peak is distributed across the MPI - ranks of a node (the persistent baseline is reported separately and is *not* included here). + Per-rank memory description of one memory-sensitive branch, split into the persistent baseline live at this + branch's peak and the transient peaks of the fast (``off``) and lean (``on``) code paths, each split by how the + transient is distributed across the MPI ranks of a node. For the node-total budget the memory on a node with ``r`` ranks at this branch's peak is ``r * (baseline + distributed) + single``: a *distributed* transient is held by every rank simultaneously (so it scales with ``r``), while a *single-rank* transient is built on one rank while the others idle (so it is counted - once). Both the fast (``off``) and lean (``on``) code paths are described. + once). When ``config.memory.use_shared_memory_giwk`` is on, the driver counts ``giwk_shareable`` once per node + instead of once per rank (subtracting ``(r - 1) * giwk_shareable`` from the node total). + :ivar baseline: Per-rank persistent bytes (full-grid two-point objects) live at this branch's peak. + :ivar giwk_shareable: The ``giwk_full`` portion of ``baseline`` that the node-shared window deduplicates to one + copy per node (0 for the Eliashberg branches, whose ``giwk_dga`` is a private per-rank object). :ivar off_distributed: Per-rank transient bytes held by every rank in the fast (flag-off) path. :ivar off_single: Transient bytes held by a single rank in the fast (flag-off) path. :ivar on_distributed: Per-rank transient bytes held by every rank in the lean (flag-on) path. :ivar on_single: Transient bytes held by a single rank in the lean (flag-on) path. """ + baseline: float + giwk_shareable: float off_distributed: float off_single: float on_distributed: float @@ -101,7 +133,7 @@ def _bubble_block(q: int, nb: int, nw: int, nv: int) -> int: def _giwk_rspace(nk_tot: int, nb: int, nv: int) -> int: """ Returns the element count of a momentum-space Green's function replicated over the full grid ``[nk_tot, nb^2, nv]`` - (the FFT paths and the persistent baseline hold such replicated buffers). + (the FFT paths and the persistent baselines hold such replicated buffers). :param nk_tot: Total number of momentum points (full BZ). :param nb: Number of bands. @@ -125,19 +157,27 @@ def estimate_peaks( with_eliashberg: bool, save_fq: bool = False, construct_fq_cheap: bool = False, + save_pairing_vertex: bool = False, + n_eig: int = 1, overhead: float = OVERHEAD_FACTOR, -) -> tuple[float, dict[str, BranchPeak]]: +) -> dict[str, BranchPeak]: r""" - Estimates the per-rank transient peak host-memory (in bytes) of the fast and lean code path of each - memory-sensitive branch, split by whether each transient is distributed across the ranks of a node or built on a - single rank, together with the per-rank persistent baseline. + Estimates the per-rank peak host-memory (in bytes) of the fast and lean code path of each memory-sensitive + branch, split by whether each transient is distributed across the ranks of a node or built on a single rank, + together with the per-rank persistent baseline live at that branch's peak. The returned dict maps a branch key to a :class:`BranchPeak`; the branch keys mirror the ``save_memory_for_*`` - switches: ``"chi0q"``, ``"chiq_aux"``, ``"sde"`` are always present; ``"fq"`` and ``"lanczos"`` are added only - when ``with_eliashberg`` is True. The first tuple element is the per-rank persistent baseline (the replicated - full-grid Green's function and self-energies that stay live throughout the non-local routine); the caller adds it - to the node total (every rank holds it). For a node with ``r`` ranks the memory at a branch's peak is - ``r * (baseline + distributed) + single``. + switches plus the flag-less ``"sde"`` step (always the two-pass FFT contraction, so its off and on slots are + identical and the driver only verifies the fit): ``"chi0q"``, ``"chiq_aux"``, ``"sde"`` are always present; + ``"fq"`` and ``"lanczos"`` are added only when ``with_eliashberg`` is True. For a node with ``r`` ranks the memory at a branch's peak is + ``r * (baseline + distributed) + single``, minus ``(r - 1) * giwk_shareable`` when the node-shared giwk window is + active (the driver assembles this; see :func:`dgamore.DGAmore.autodetect_memory_settings`). + + The per-branch baselines track the actual giwk window of ``nonlocal_sde.calculate_self_energy_q``: the bubble + (``chi0q``) runs on the ``niv_cut`` window, after which giwk is cut (and re-shared) to the + ``niv_core + niw_core`` window for the kernel/SDE section (``chiq_aux``, ``sde``); ``sigma_old`` is cut to the + core box before the kernel section. The Eliashberg branches run after the self-consistency loop on the private + per-rank ``giwk_dga``/``sigma_dga`` pair at the ``niv_cut`` window (not node-shared, hence ``giwk_shareable = 0``). :param n_bands: Number of bands :math:`B`. :param nk_tot: Total number of momentum points (full BZ). @@ -145,59 +185,78 @@ def estimate_peaks( :param niw_core: Number of positive bosonic core frequencies. :param niv_core: Number of positive fermionic core frequencies. :param niv_full: Number of positive fermionic full-region frequencies. - :param niv_cut: Number of positive fermionic frequencies the full-grid ``giwk_full`` is kept at through the - kernel/SDE section (``min(niw_core + niv_full + 10, niv_dmft)`` in - :func:`dgamore.nonlocal_sde.calculate_self_energy_q`); the SDE self-energy contraction needs the shell window, - so giwk is not shrunk to the core box here. + :param niv_cut: Number of positive fermionic frequencies the full-grid ``giwk_full`` is built at + (``min(niw_core + niv_full + 10, niv_dmft)`` in :func:`dgamore.nonlocal_sde.calculate_self_energy_q`). :param niv_pp: Number of positive fermionic frequencies of the pp (Eliashberg) box. :param n_ranks: Number of MPI ranks the q-points are distributed over. :param with_eliashberg: Whether the Eliashberg step runs (adds the ``"fq"`` and ``"lanczos"`` branches). :param save_fq: Whether the full ladder vertex is kept in the full ph box (``config.eliashberg.save_fq``); when - True the per-rank ``fq`` accumulator spans the full ``[wn, vc, vc]`` block instead of the small pp box. + True the per-rank ``fq`` accumulator spans the full ``[wn, vc, vc]`` block instead of the small pp box AND the + whole irreducible-BZ vertex is gathered on one rank for saving (a single-rank peak in both paths). :param construct_fq_cheap: Whether the ``fq`` per-q blocks are built on the smaller pp frequency box (``config.eliashberg.construct_fq_cheap``), shrinking every per-q two-fermion block from ``vc`` to ``vpp``. + :param save_pairing_vertex: Whether both pp pairing vertices are gathered on one rank for saving + (``config.eliashberg.save_pairing_vertex``); a single-rank peak of the ``lanczos`` branch. + :param n_eig: Number of requested eigenpairs (``config.eliashberg.n_eig``); sets the ARPACK Lanczos basis size + ``ncv = max(2 * n_eig + 1, 20)`` held per solving rank. :param overhead: Global multiplicative factor accounting for un-modeled transient arrays. - :return: A tuple ``(baseline_bytes, peaks)`` of the per-rank baseline and a dict mapping each branch key to its - :class:`BranchPeak`. + :return: A dict mapping each branch key to its :class:`BranchPeak`. """ nb = n_bands wp = niw_core + 1 # half bosonic range, as the heavy objects are constructed vc = 2 * niv_core vf = 2 * niv_full vpp = 2 * niv_pp + niv_sde = niv_core + niw_core # giwk window through the kernel/SDE section (post-bubble cut/re-share) qi = _ceil_div(nk_irr, n_ranks) # per-rank irreducible-BZ q-count qt = _ceil_div(nk_tot, n_ranks) # per-rank full-BZ q-count scale = DTYPE_BYTES * overhead - # Per-rank persistent baseline at the heavy-section (kernel/SDE) peak: the two full-grid two-point objects that - # stay live on every rank -- giwk_full and sigma_old, both kept at niv_cut through the SDE (giwk's shell window is - # needed by the self-energy contraction; sigma_old keeps its DMFT shell for the mixing/residual). See - # nonlocal_sde.calculate_self_energy_q. The remaining self-energies (sigma_dmft, sigma_dmft_full, delta_sigma) are - # local (a single k-point) and negligible. - baseline = scale * 2 * _giwk_rspace(nk_tot, nb, 2 * niv_cut) + # Persistent baselines (full-grid two-point objects live on every rank at the branch peak). During the bubble, + # giwk_full and sigma_old are both at the niv_cut window; before the kernel section giwk is cut (and re-shared) + # to the niv_core + niw_core window and sigma_old to the core box. The remaining self-energies (sigma_dmft, + # sigma_dmft_full, delta_sigma) are local (a single k-point) and negligible. The Eliashberg step runs after the + # loop on the per-rank giwk_dga + sigma_dga pair at the niv_cut window (never node-shared). + giwk_bubble = scale * _giwk_rspace(nk_tot, nb, 2 * niv_cut) + giwk_sde = scale * _giwk_rspace(nk_tot, nb, 2 * niv_sde) + baseline_bubble = giwk_bubble + scale * _giwk_rspace(nk_tot, nb, 2 * niv_cut) # + sigma_old at niv_cut + baseline_sde = giwk_sde + scale * _giwk_rspace(nk_tot, nb, vc) # + sigma_old at the core box + baseline_eliashberg = scale * 2 * _giwk_rspace(nk_tot, nb, 2 * niv_cut) # giwk_dga + sigma_dga peaks: dict[str, BranchPeak] = {} # chi0q: fast path (FFT, create_generalized_chi0_q_fft) builds the WHOLE irreducible-BZ bubble on rank 0 - # (nk_irr, not the per-rank q-count) plus TWO full-grid B^4 buffers, each with one fermionic axis over nk_tot -- - # the preallocated ``chi_r_v_buffer`` multiply target and the equally large array returned by ``xp.fft.ifftn`` - # each iw -- and ~2 replicated real-space Green's functions over the (niv_full + niw_core) window. All on rank 0, - # so the whole fast path is a SINGLE-rank transient. The lean per-q einsum builds only this rank's q-slice of - # the bubble plus its own ~2 Green's-function buffers, so it is DISTRIBUTED. - gf_copies = 2 * _giwk_rspace(nk_tot, nb, 2 * (niv_full + niw_core)) + # (nk_irr, not the per-rank q-count) plus the full-grid B^4 multiply buffer ``chi_r_v_buffer`` AND the + # ~2x-buffer-sized ``xp.fft.ifftn`` transient each iw, plus the replicated real-space Green's functions: two at + # the (niv_full + niw_core) window (g_k stays bound through the loop, g_r_rev is its flipped/transposed copy) and + # the central niv_full window slice (g_r). All on rank 0, so the whole fast path is a SINGLE-rank transient. The + # lean per-q einsum builds only this rank's q-slice of the bubble plus its two shared-backing Green's-function + # buffers (g_full + g_r_buf), so it is DISTRIBUTED. + gf_window_bubble = 2 * (niv_full + niw_core) peaks["chi0q"] = BranchPeak( + baseline=baseline_bubble, + giwk_shareable=giwk_bubble, off_distributed=0.0, - off_single=scale * (_bubble_block(nk_irr, nb, wp, vf) + 2 * _bubble_block(nk_tot, nb, 1, vf) + gf_copies), - on_distributed=scale * (_bubble_block(qi, nb, wp, vf) + gf_copies), + off_single=scale + * ( + _bubble_block(nk_irr, nb, wp, vf) + + (1 + CHI0Q_IFFTN_TRANSIENT_FACTOR) * _bubble_block(nk_tot, nb, 1, vf) + + 2 * _giwk_rspace(nk_tot, nb, gf_window_bubble) + + _giwk_rspace(nk_tot, nb, vf) + ), + on_distributed=scale * (_bubble_block(qi, nb, wp, vf) + 2 * _giwk_rspace(nk_tot, nb, gf_window_bubble)), on_single=0.0, ) # chiq_aux: fast path (v1) materializes the whole rank-local two-fermion block on every rank (DISTRIBUTED) and - # inverts it one q at a time, plus the full-BZ kernel gathered on a SINGLE rank (rank 0). The lean path (v3) - # builds one q at a time and accumulates the (1-fermion) summed result, all DISTRIBUTED. + # inverts it one q at a time, plus the full-BZ kernel assembled on a SINGLE rank (rank 0) by the gather/unfold + # irr-to-full-BZ map this flag selects (map_irrbz_fullbz, run per FFT-SDE pass). The lean path (v3) builds one q + # at a time and accumulates the (1-fermion) summed result, all DISTRIBUTED (p2p map, no single-rank assembly). peaks["chiq_aux"] = BranchPeak( + baseline=baseline_sde, + giwk_shareable=giwk_sde, off_distributed=scale * CHIQ_AUX_INVERT_FACTOR * _two_fermion_block(qi, nb, wp, vc), off_single=scale * _bubble_block(nk_tot, nb, wp, vc), on_distributed=scale @@ -205,46 +264,83 @@ def estimate_peaks( on_single=0.0, ) - # sde: both paths are DISTRIBUTED. The fast path (FFT) materializes the full-BZ kernel on every rank plus a - # replicated R-space Green's function (giwk is kept at niv_cut by this point) and the kernel's own R-space buffer. - # The lean q-loop (calculate_sigma_from_kernel_cpu) maps the kernel once but then makes a full - # ``np.asfortranarray(giwk.mat)`` copy of the Green's function (and a Fortran copy of the kernel of comparable - # size to the mapped one), so beyond the mapped kernel it holds a replicated full-grid Green's function at - # niv_cut. Both paths carry a comparable kernel transient (the FFT path redistributes it, the q-loop copies it), - # so only the giwk copy is counted here. - sde_kernel_full = _bubble_block(qt, nb, wp, vc) + # sde: a single (flag-less) path - the two-pass FFT contraction. It keeps the mapped full-BZ niw-half kernel + # plus its half-niv copy in flight through the conj/distributed-FFT round trip (~SDE_FFT_KERNEL_FACTOR kernel + # blocks), retains the irreducible-BZ kernel across both passes, and makes one private real-space + # Green's-function copy (giwk.fft(copy=True)) per rank at the SDE window; rank 0 additionally finalizes the + # gathered self-energy (ifft + to_full_niv_range, ~2 full-grid nb^2 objects at the core box). The old q-loop + # path is unused (it restored the FULL bosonic range on the kernel and peaked HIGHER - see + # nonlocal_sde.calculate_sigma_from_kernel), so no save_memory switch exists and both path slots carry the FFT + # path; the driver only verifies the fit. + sde_distributed = scale * ( + SDE_FFT_KERNEL_FACTOR * _bubble_block(qt, nb, wp, vc) + + _bubble_block(qi, nb, wp, vc) + + _giwk_rspace(nk_tot, nb, 2 * niv_sde) + ) + sde_single = scale * 2 * _giwk_rspace(nk_tot, nb, vc) peaks["sde"] = BranchPeak( - off_distributed=scale * (sde_kernel_full + 2 * _giwk_rspace(nk_tot, nb, 2 * niv_cut)), - off_single=0.0, - on_distributed=scale * (sde_kernel_full + _giwk_rspace(nk_tot, nb, 2 * niv_cut)), - on_single=0.0, + baseline=baseline_sde, + giwk_shareable=giwk_sde, + off_distributed=sde_distributed, + off_single=sde_single, + on_distributed=sde_distributed, + on_single=sde_single, ) if with_eliashberg: - # fq: both paths are DISTRIBUTED. The fast path builds the whole rank-local two-fermion ph block and combines - # it with whole-block compound matmuls, holding ~FQ_MATMUL_FACTOR full blocks live; the lean path does the - # same one q at a time but additionally writes into a rank-local accumulator (f_q_r_mat) spanning ALL - # rank-local q-points. ``construct_fq_cheap`` shrinks every per-q construction block from vc to vpp; the - # accumulator keeps the full ph box [wn, vc, vc] when ``save_fq`` is set, otherwise the small pp box [vpp, vpp]. + # fq: both paths hold ~FQ_MATMUL_FACTOR two-fermion blocks at the matmul-chain peak (per rank for the fast + # path, per q for the lean one) plus the loaded 1-fermion inputs (gchi0_q_inv for the fast path; gchi0_q_inv, + # vrg_left and vrg_right for the lean one, which keeps all three alive through its q-loop) and, for the lean + # path, the rank-local accumulator. ``construct_fq_cheap`` shrinks every per-q construction block from vc to + # vpp; the accumulator keeps the full ph box [wn, vc, vc] when ``save_fq`` is set, otherwise the small pp box + # [vpp, vpp]. ``save_fq`` additionally gathers the WHOLE irreducible-BZ two-fermion vertex on one rank for + # saving - a single-rank peak in BOTH paths (usually the largest single object of a save_fq run). vc_fq = vpp if construct_fq_cheap else vc fq_accumulator = _two_fermion_block(qi, nb, wp, vc) if save_fq else _two_fermion_block(qi, nb, 1, vpp) + fq_gather_single = scale * _two_fermion_block(nk_irr, nb, wp, vc) if save_fq else 0.0 peaks["fq"] = BranchPeak( - off_distributed=scale * FQ_MATMUL_FACTOR * _two_fermion_block(qi, nb, wp, vc_fq), - off_single=0.0, - on_distributed=scale * (FQ_MATMUL_FACTOR * _two_fermion_block(1, nb, wp, vc_fq) + fq_accumulator), - on_single=0.0, + baseline=baseline_eliashberg, + giwk_shareable=0.0, + off_distributed=scale + * (FQ_MATMUL_FACTOR * _two_fermion_block(qi, nb, wp, vc_fq) + _bubble_block(qi, nb, wp, vc_fq)), + off_single=fq_gather_single, + on_distributed=scale + * ( + FQ_MATMUL_FACTOR * _two_fermion_block(1, nb, wp, vc_fq) + + fq_accumulator + + 3 * _bubble_block(qi, nb, wp, vc_fq) + ), + on_single=fq_gather_single, ) - # lanczos: the fast (in-memory) path assembles the entire BZ pairing vertex on ONE rank AND a momentum-flipped - # copy of it (flip_momentum_axis allocates a fresh full array), so two full-BZ vertices are live on a SINGLE - # rank. The lean path (gather_full_ibz_for_vslice + map_to_full_bz) hands every rank the FULL BZ with only a - # slice of the second fermionic frequency, so its per-rank share scales with the full-BZ per-rank q-count - # (nk_tot/n_ranks), NOT the irreducible one -- also two copies (vertex + flipped), DISTRIBUTED. + # lanczos: the fast (in-memory) path assembles the entire-BZ pairing vertex on ONE solving rank and holds + # LANCZOS_VERTEX_FACTOR copies of it at the matmul-layout build (direct + flipped + one layout copy), plus + # the full-BZ pp bubble and the ARPACK workspace (the ncv-column Lanczos basis + gap-sized matvec + # temporaries). The singlet and triplet solves run CONCURRENTLY on two ranks - the driver doubles this + # single-rank peak when both land on the same node; a single-rank run solves sequentially but holds the + # waiting channel's gathered irreducible-BZ vertex alongside. The lean path (gather_full_ibz_for_vslice) + # hands every active rank the FULL BZ with only its slice of the second fermionic frequency (the v-axis has + # only 2*niv_pp tasks, hence the ceil-based share), also LANCZOS_VERTEX_FACTOR copies at the layout build, + # plus the same per-rank ARPACK workspace (every active rank runs its own full-length eigsh); the root rank + # additionally holds the full-BZ pp bubble. ``save_pairing_vertex`` gathers both irreducible-BZ pp vertices + # on rank 0 before the solve (sequential, hence max() against the solver single-rank peak). + vertex_pp_full = _two_fermion_block(nk_tot, nb, 1, vpp) + chi0_pp_full = _bubble_block(nk_tot, nb, 1, vpp) + ncv = max(2 * n_eig + 1, 20) + arpack_ws = (ncv + ARPACK_EXTRA_VECTORS) * _giwk_rspace(nk_tot, nb, vpp) + pairing_gather = 2 * _two_fermion_block(nk_irr, nb, 1, vpp) if save_pairing_vertex else 0 + v_share = _ceil_div(vpp, n_ranks) # per-rank share of the v'-distributed vertex (vpp tasks, not q-points) + vertex_pp_slice = nk_tot * nb**4 * vpp * v_share + solver_single_fast = LANCZOS_VERTEX_FACTOR * vertex_pp_full + chi0_pp_full + arpack_ws + if n_ranks == 1: + solver_single_fast += _two_fermion_block(nk_irr, nb, 1, vpp) # the waiting channel's gathered vertex peaks["lanczos"] = BranchPeak( + baseline=baseline_eliashberg, + giwk_shareable=0.0, off_distributed=0.0, - off_single=scale * 2 * _two_fermion_block(nk_tot, nb, 1, vpp), - on_distributed=scale * 2 * _two_fermion_block(qt, nb, 1, vpp), - on_single=0.0, + off_single=scale * max(solver_single_fast, pairing_gather), + on_distributed=scale * (LANCZOS_VERTEX_FACTOR * vertex_pp_slice + arpack_ws), + on_single=scale * max(chi0_pp_full, pairing_gather), ) - return baseline, peaks + return peaks diff --git a/dgamore/mpi_utils.py b/dgamore/mpi_utils.py index 58d4dc3e..25503456 100644 --- a/dgamore/mpi_utils.py +++ b/dgamore/mpi_utils.py @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ -Multiprocessing (MPI) utilities for the non-local step — a single module covering everything parallel: +Multiprocessing (MPI) utilities for the non-local step - a single module covering everything parallel: * low-level **message-chunking primitives** (``send_rows`` / ``recv_rows_into`` / ``recv_rows_alloc`` / ``bcast_rows`` / ``bcast_rows_into`` / ``send_bytes`` / ``recv_bytes`` and the ``row_chunks`` / ``chunk_step`` @@ -37,12 +37,50 @@ from dgamore import symmetry_reduction from dgamore.brillouin_zone import KGrid from dgamore.four_point import FourPoint +from dgamore.n_point_base import DTYPE # Canonical 2 GB MPI per-message limit. The chunking helpers below take it as an explicit ``limit`` argument so the # established test hook of monkeypatching ``mpi_utils.MAX_MPI_BYTES`` to force the chunked path keeps working. MAX_MPI_BYTES = 2**31 - 1 +def build_node_shared_array(node_comm, compute_fn, dtype=DTYPE): + r""" + Build an array once per node and expose it to every rank on that node through a single MPI shared-memory window, + so a large replicated quantity (e.g. the full-grid Green's function ``giwk_full``) is stored **once per node + instead of once per rank** and computed only on the node root. + + ``compute_fn`` is called **only on the node-local root rank** (rank 0 of ``node_comm``) and must return the fully + built numpy array; the other ranks do not call it. Its result is copied into a shared-memory segment allocated by + the root, and every rank receives a numpy view of that same physical buffer (read-only by convention - only the + root writes it). ``node_comm`` must be a node-local communicator, e.g. ``comm.Split_type(MPI.COMM_TYPE_SHARED)``. + + When the node holds a single rank the shared window is pointless, so the freshly computed private array is + returned unchanged with ``win = None`` (this also keeps single-rank / mock communicators working). The caller + owns the returned window and must free it (``win.Free()``) once all ranks are done reading the array. + + :param node_comm: The node-local (shared-memory) communicator. + :param compute_fn: Zero-argument callable returning the array; invoked only on the node root. + :param dtype: Storage dtype of the shared buffer (defaults to the global ``DTYPE``, complex64). + :return: The tuple ``(array, win)`` - the (shared) numpy array on every rank and the MPI window (``None`` for a + single-rank node). + """ + is_root = node_comm.Get_rank() == 0 + local = compute_fn() if is_root else None + if node_comm.Get_size() == 1: + return local, None + shape = node_comm.bcast(local.shape if is_root else None) + itemsize = np.dtype(dtype).itemsize + nbytes = int(np.prod(shape)) * itemsize if is_root else 0 + win = MPI.Win.Allocate_shared(nbytes, itemsize, comm=node_comm) + buf, _ = win.Shared_query(0) + shared = np.ndarray(buffer=buf, dtype=dtype, shape=shape) + if is_root: + shared[...] = local + node_comm.Barrier() + return shared, win + + # ==================================================================================================================== # Message-chunking primitives (split a transfer's leading axis below the 2 GB MPI per-message limit). # ==================================================================================================================== @@ -658,7 +696,7 @@ def bcast_npoint(self, obj, root: int = 0): """ Broadcasts an N-point-like object (one exposing a ``.mat`` numpy array) from ``root`` to all ranks. The large ``.mat`` is broadcast as raw sub-2 GB chunks (so there is no multi-gigabyte pickle blob and no >2 GB message), - while the rest of the object travels as a small pickled metadata blob — the broadcast analogue of + while the rest of the object travels as a small pickled metadata blob - the broadcast analogue of :meth:`send_to_rank`/:meth:`recv_from_rank`. Prefer this over :meth:`bcast` for large objects such as a full-BZ self-energy or gap function, both to respect the 2 GB limit and to avoid the full in-memory pickle copy. @@ -692,7 +730,7 @@ def allreduce(self, rank_result=None) -> np.ndarray: ``Allreduce`` is collective, so the chunk schedule must be identical on every rank. That holds here because the reduced arrays are always equally shaped across ranks (the callers reduce full, replicated quantities such as - the full-k-space self-energy / Fock term — each rank holds a partial sum of the *same* array), so every rank + the full-k-space self-energy / Fock term - each rank holds a partial sum of the *same* array), so every rank derives the same chunk boundaries. The single-chunk case is byte-for-byte the previous behavior. :param rank_result: This rank's contribution; reduced in place. Must have the same shape on every rank. diff --git a/dgamore/n_point_base.py b/dgamore/n_point_base.py index 5dbad618..aaee4884 100644 --- a/dgamore/n_point_base.py +++ b/dgamore/n_point_base.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Foundational mixins for every physical quantity in the code. :class:`IHaveMat` owns the underlying numpy array @@ -159,6 +159,16 @@ def memory_usage_in_gb(self) -> float: """ return self.mat.nbytes / (1024**3) + def copy(self) -> "IHaveMat": + """ + Returns an independent deep copy of the object -- a thin wrapper around :func:`copy.deepcopy` used in place of + a bare ``deepcopy(obj)`` throughout the package. Mutating the returned object (including its ``mat``) does not + affect the original. + + :return: A deep copy of ``self``. + """ + return deepcopy(self) + def __mul__(self, other) -> "IHaveMat": """ Multiplies the object by a scalar number, returning a new (deep-copied) object. @@ -170,7 +180,7 @@ def __mul__(self, other) -> "IHaveMat": if not isinstance(other, (int, float, complex)): raise ValueError("Multiplication only supported with numbers.") - copy = deepcopy(self) + copy = self.copy() copy.mat *= other return copy @@ -203,6 +213,26 @@ def __truediv__(self, other) -> "IHaveMat": raise ValueError("Division only supported with numbers.") return self.__mul__(1.0 / other) + def scale(self, factor, copy: bool = False) -> "IHaveMat": + """ + Multiplies the matrix by a scalar. The in-place branch (``copy=False``, the default here) mutates and + returns ``self`` without allocating a copy -- the memory-lean counterpart of the non-destructive + ``*``/:meth:`__mul__` operator (which already covers the copying case). Used to fold a scalar prefactor + into an in-place accumulation (e.g. the self-energy-kernel assembly in :mod:`dgamore.nonlocal_sde`) + without a throwaway scaled copy. ``copy=True`` returns a scaled deep copy and leaves ``self`` untouched. + + :param factor: A scalar (int, float or complex) to multiply with. + :param copy: If True, return a scaled deep copy; if False (default), scale ``self.mat`` in place. + :return: The scaled object (``self`` when ``copy=False``). + :raises ValueError: If ``factor`` is not a number. + """ + if not isinstance(factor, (int, float, complex)): + raise ValueError("Multiplication only supported with numbers.") + if copy: + return self.copy().scale(factor, copy=False) + self.mat *= factor + return self + def __getitem__(self, item): """ Indexing shortcut: ``obj[item]`` is equivalent to ``obj.mat[item]``. @@ -291,7 +321,7 @@ def _clone_without_mat(self): mat = self._mat self._mat = None try: - clone = deepcopy(self) + clone = self.copy() finally: self._mat = mat return clone @@ -705,7 +735,7 @@ def interpolate_q_grid(self, nq_new: tuple[int, int, int], copy: bool = True): :return: The re-gridded object in the same momentum-compression state as the input. :raises ValueError: If a target size is not a positive integer, or the object is compressed but not full-BZ. """ - copy = deepcopy(self) if copy else self + copy = self.copy() if copy else self nq_new = tuple(int(n) for n in nq_new) if any(n < 1 for n in nq_new): @@ -753,12 +783,12 @@ def _map_to_full_bz(self, k_grid: "KGrid", num_orbital_dimensions: int, nq: tupl per-k orbital transformation stored on ``k_grid`` by ``specify_auto_symmetries(hk)``. The orbital transformation follows the ket/bra convention of the operator ordering - :math:`G_{abcd} := \langle T[c_a c^\dagger_b c_c c^\dagger_d]\rangle` -- annihilation indices + :math:`G_{1234} := \langle T[c_1 c^\dagger_2 c_3 c^\dagger_4]\rangle` -- annihilation indices (positions 1, 3) transform with :math:`U`, creation indices (positions 2, 4) with :math:`U^\dagger` -- combined with a per-k antisymmetry sign :math:`\sigma_k` and an optional complex conjugation ``conj_k``: - 2-index : :math:`M_{ab}(k) = \sigma_k \, U_{aa'} [M_{a'b'}(k_{rep})]^{[*conj_k]} U^\dagger_{b'b}` - 4-index : :math:`M_{abcd}(k) = \sigma_k^2 \, U_{aa'} [M_{a'b'c'd'}(k_{rep})]^{[*conj_k]} U^\dagger_{b'b} U_{cc'} U^\dagger_{d'd}` + 2-index : :math:`M_{12}(k) = \sigma_k \, U_{1a} [M_{ab}(k_{rep})]^{[*conj_k]} U^\dagger_{b2}` + 4-index : :math:`M_{1234}(k) = \sigma_k^2 \, U_{1a} [M_{abcd}(k_{rep})]^{[*conj_k]} U^\dagger_{b2} U_{3c} U^\dagger_{d4}` If ``k_grid`` is not yet in auto mode (``specify_auto_symmetries`` has not been called), only the momentum expansion is performed and orbital indices are left unchanged. @@ -807,7 +837,7 @@ def fft(self, copy: bool = True): :return: The Fourier-transformed object, in the same momentum-compression state as the input. """ if copy: - return deepcopy(self).fft(copy=False) + return self.copy().fft(copy=False) compress = False if self.has_compressed_q_dimension: @@ -826,7 +856,7 @@ def ifft(self, copy: bool = True): :return: The inverse-Fourier-transformed object, in the same momentum-compression state as the input. """ if copy: - return deepcopy(self).ifft(copy=False) + return self.copy().ifft(copy=False) compress = False if self.has_compressed_q_dimension: diff --git a/dgamore/nonlocal_sde.py b/dgamore/nonlocal_sde.py index b98b1bc5..2eff87ab 100644 --- a/dgamore/nonlocal_sde.py +++ b/dgamore/nonlocal_sde.py @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" -Non-local ladder DGA step — the parallel-heavy core of the code. Starting from the local irreducible vertex +Non-local ladder DGA step - the parallel-heavy core of the code. Starting from the local irreducible vertex :math:`\Gamma_{r}` and the bare interaction, the functions here build, per momentum :math:`q` and spin channel, the bubble :math:`\chi_0^q`, the auxiliary susceptibility :math:`\chi^{*;q}_{r}`, the three-leg vertex :math:`\gamma^q_{r}`, the physical susceptibility :math:`\chi^q_{r}` (with shell and optional @@ -18,7 +18,6 @@ import glob import os import re -from copy import deepcopy import mpi4py.MPI as MPI import numpy as np @@ -214,17 +213,38 @@ def create_auxiliary_chi_r_q_sum_v3( def create_vrg_r_q(gchi_aux_q_r_sum: FourPoint, gchi0_q_inv: FourPoint) -> FourPoint: r""" Returns the momentum-dependent three-leg vertex, see Eq. (3.63) in my master's thesis, - :math:`\gamma_{r;abcd}^{q\nu} = \beta (\chi^{q\nu\nu}_{0;ablm})^{-1} (\sum_{\nu'} \chi^{*;q\nu\nu'}_{r;mlcd})`. + :math:`\gamma_{r;1234}^{q\nu} = \beta \sum_{ab} \sum_{\nu'} (\chi^{q\nu}_{0;12ab})^{-1} \chi^{*;q\nu\nu'}_{r;ba34}`. - :param gchi_aux_q_r_sum: The frequency-summed auxiliary susceptibility :math:`\sum_{\nu'}\chi^{*;q}_{r}`. + :param gchi_aux_q_r_sum: The frequency-summed auxiliary susceptibility :math:`\sum_{\nu'}\chi^{*;q\nu\nu'}_{r}`. :param gchi0_q_inv: The inverse bare bubble :math:`(\chi_0^q)^{-1}` (core box). :return: The three-leg vertex :math:`\gamma^q_{r}` (``vrg``) as a :class:`FourPoint`. """ - return config.sys.beta * (gchi0_q_inv @ gchi_aux_q_r_sum) + return (gchi0_q_inv @ gchi_aux_q_r_sum).scale(config.sys.beta) + + +def create_vrg_r_q_right(gchi_aux_q_r_sum: FourPoint, gchi0_q_inv: FourPoint) -> FourPoint: + r""" + Returns the momentum-dependent right-sided three-leg vertex, i.e. the counterpart of :meth:`create_vrg_r_q` + (Eq. (3.63) in my master's thesis) with the summed frequency argument of :math:`\chi^{*}` and the position of + :math:`(\chi_0)^{-1}` swapped. It thus reads + :math:`\tilde{\gamma}_{r;1234}^{q\nu} = \beta \sum_{ab} \sum_{\nu'} \chi^{*;q\nu'\nu}_{r;12ab} (\chi^{q\nu}_{0;ba34})^{-1}`. + Notice that the sum runs over the *first* frequency argument, whereas only the sum over the last frequency is + available (see :meth:`FourPoint.invert_and_sum_over_last_vn`). The two are related by the time-reversal symmetry + :math:`\chi^{*;q\nu\nu'}_{r;1234} = \chi^{*;q\nu'\nu}_{r;4321}` (enforced on the DMFT two-particle Green's + function via :meth:`LocalFourPoint.symmetrize_v_vp` and inherited by all vertices built from it), which carries + an orbital reversal along with the frequency swap: + :math:`\sum_{\nu'} \chi^{*;q\nu'\nu}_{r;12ab} = \sum_{\nu'} \chi^{*;q\nu\nu'}_{r;ba21}`. Hence the last-frequency + sum enters with the orbital permutation ``"abcd->dcba"`` applied. + + :param gchi_aux_q_r_sum: The frequency-summed auxiliary susceptibility :math:`\sum_{\nu'}\chi^{*;q\nu\nu'}_{r}`. + :param gchi0_q_inv: The inverse bare bubble :math:`(\chi_0^q)^{-1}` (core box). + :return: The right-sided three-leg vertex :math:`\tilde{\gamma}^q_{r}` (``vrg_right``) as a :class:`FourPoint`. + """ + return (gchi_aux_q_r_sum.permute_orbitals("abcd->dcba") @ gchi0_q_inv).scale(config.sys.beta) def create_generalized_chi_q_with_shell_correction( - gchi_aux_q_sum: FourPoint, + chi_phys_q_r: FourPoint, gchi0_q_full_sum: FourPoint, gchi0_q_core_sum: FourPoint, u_loc: LocalInteraction, @@ -235,7 +255,7 @@ def create_generalized_chi_q_with_shell_correction( Motoharu Kitatani et al. 2022 J. Phys. Mater. 5 034005; DOI 10.1088/2515-7639/ac7e6d. Eq. A.15. See also Sec. 3.7.2 in my master's thesis for details. - :param gchi_aux_q_sum: The frequency-summed auxiliary susceptibility :math:`\sum_{\nu\nu'}\chi^{*;q}_{r}`. + :param chi_phys_q_r: The physical susceptibility :math:`\chi^{phys;q}_{r}`. :param gchi0_q_full_sum: The frequency-summed bare bubble over the full box. :param gchi0_q_core_sum: The frequency-summed bare bubble over the core box. :param u_loc: The bare local interaction :math:`U`. @@ -243,8 +263,8 @@ def create_generalized_chi_q_with_shell_correction( :return: The shell-corrected physical susceptibility :math:`\chi^{q}_{r}` as a :class:`FourPoint`. """ return ( - (gchi_aux_q_sum + gchi0_q_full_sum - gchi0_q_core_sum).invert() - + (u_loc.as_channel(gchi_aux_q_sum.channel) + v_nonloc.as_channel(gchi_aux_q_sum.channel)) + (chi_phys_q_r + gchi0_q_full_sum - gchi0_q_core_sum).invert() + + (u_loc.as_channel(chi_phys_q_r.channel) + v_nonloc.as_channel(chi_phys_q_r.channel)) ).invert() @@ -270,7 +290,7 @@ def calculate_sigma_dc_kernel(f_dc_loc: LocalFourPoint, gchi0_q: FourPoint, u_lo def calculate_kernel_r_q( - vrg_q_r: FourPoint, gchi_aux_q_r_sum: FourPoint, v_nonloc: Interaction, u_loc: LocalInteraction + vrg_q_r: FourPoint, chi_phys_q_r: FourPoint, v_nonloc: Interaction, u_loc: LocalInteraction ) -> FourPoint: r""" Returns the kernel for the self-energy calculation minus 2/3 times the identity if the channel is the magnetic @@ -279,16 +299,16 @@ def calculate_kernel_r_q( .. math:: K = \gamma_{r;abcd}^{q\nu} - \gamma_{r;abef}^{q\nu} U^{q}_{r;fehg} \chi_{r;ghcd}^{q}. :param vrg_q_r: The momentum-dependent three-leg vertex :math:`\gamma^q_{r}`. - :param gchi_aux_q_r_sum: The (shell-corrected) physical susceptibility :math:`\chi^{q}_{r}`. + :param chi_phys_q_r: The (shell-corrected) physical susceptibility :math:`\chi^{phys;q}_{r}`. :param v_nonloc: The non-local interaction :math:`V^{q}`. :param u_loc: The bare local interaction :math:`U`. :return: The self-energy kernel :math:`U_r K` as a :class:`FourPoint`. """ u_r = v_nonloc.as_channel(vrg_q_r.channel) + u_loc.as_channel(vrg_q_r.channel) - kernel = vrg_q_r - vrg_q_r @ u_r @ gchi_aux_q_r_sum + kernel = vrg_q_r - vrg_q_r @ u_r @ chi_phys_q_r if vrg_q_r.channel == SpinChannel.MAGN: - kernel -= 2.0 / 3.0 * FourPoint.identity_like(kernel) + kernel.sub(FourPoint.identity_like(kernel).scale(2.0 / 3.0), copy=False) return u_r @ kernel @@ -334,7 +354,7 @@ def fit_oz_spin(q_grid: KGrid, mat: np.ndarray): initial_guess = (mat.max(), 2.0) return opt.curve_fit(oz_spin_w0, q_grid, mat, p0=initial_guess)[0] - chi = deepcopy(chi_phys_q_r) + chi = chi_phys_q_r.copy() chi_mat = chi.map_to_full_bz(config.lattice.q_grid).to_half_niw_range().take_first_wn().mat.real orb_shape = (config.sys.n_bands,) * 4 oz_coeffs = np.zeros(orb_shape + (2,), dtype=float) @@ -425,12 +445,20 @@ def calculate_sigma_kernel_r_q( mpi_dist_irrq.barrier() logger.log_memory_usage( - f"Gchi_aux ({gchi_aux_q_r_sum.channel.value})", + f"Auxiliary susceptibility ({gchi_aux_q_r_sum.channel.value})", gchi_aux_q_r_sum, mpi_dist_irrq.comm.size * 2 * config.box.niv_core, ) logger.info(f"Non-Local auxiliary susceptibility ({gchi_aux_q_r_sum.channel.value}) calculated.") + if config.eliashberg.perform_eliashberg: + vrg_q_r_right = create_vrg_r_q_right(gchi_aux_q_r_sum, gchi0_q_inv) + vrg_q_r_right.save( + name=f"vrg_q_{vrg_q_r_right.channel.value}_right_rank_{mpi_dist_irrq.comm.rank}", + output_dir=config.output.eliashberg_path, + ) + vrg_q_r_right.free() + vrg_q_r = create_vrg_r_q(gchi_aux_q_r_sum, gchi0_q_inv) logger.info(f"Non-local three-leg vertex gamma^wv ({vrg_q_r.channel.value}) done.") @@ -476,7 +504,7 @@ def calculate_sigma_kernel_r_q( if config.eliashberg.perform_eliashberg: chi_phys_q_r.save( - name=f"gchi_aux_q_{chi_phys_q_r.channel.value}_sum_rank_{mpi_dist_irrq.comm.rank}", + name=f"chi_phys_q_{chi_phys_q_r.channel.value}_rank_{mpi_dist_irrq.comm.rank}", output_dir=config.output.eliashberg_path, ) @@ -556,10 +584,15 @@ def perform_lambda_correction(chi_phys_q_r: FourPoint) -> FourPoint: def calculate_sigma_from_kernel(kernel: FourPoint, giwk: GreensFunction, my_full_q_list: np.ndarray) -> SelfEnergy: r""" - Returns :math:`\Sigma_{ij}^{k} = -\frac{1}{2\beta N_q} \sum_q U^q_{r;aibc} K_{r;cbjd}^{q\nu} G_{ad}^{\omega-\nu}`. + Returns :math:`\Sigma_{12}^{k\nu} = -\frac{1}{2\beta N_q} \sum_{q\omega} \sum_{abcd} U^q_{r;a1bc} K_{r;cb2d}^{q\omega\nu} G_{ad}^{\nu-\omega}`. For very large momentum grids, this function is the slowest part compared to the rest of the code due to the repeated loops. Potential speed-ups could be achieved by batching the q-points or using numba. + Currently unused: the pipeline always runs the two-pass FFT contraction (see + :func:`calculate_sigma_from_kernel_fft_cpu`), since this q-loop variant restores the full bosonic range on the + kernel and therefore peaks *higher* in memory. Kept (with its cpu/gpu/auto siblings) for reference; the + variants' mutual parity is unit-tested. + :param kernel: The self-energy kernel :math:`K` (full BZ, scattered across ranks). :param giwk: The momentum-dependent :class:`GreensFunction`. :param my_full_q_list: Array of integer q-point index triplets handled by this rank. @@ -572,7 +605,7 @@ def calculate_sigma_from_kernel(kernel: FourPoint, giwk: GreensFunction, my_full kernel = kernel.to_full_niw_range() wn = MFHelper.wn(config.box.niw_core) - path = np.einsum_path("aijdv,xyzadv->xyzijv", kernel[0, ..., 0, :], mat, optimize=True)[1] + path = np.einsum_path("aijdv,xyzadv->xyzijv", kernel[0, ..., 0, config.box.niv_core :], mat, optimize=True)[0] for idx_q, q in enumerate(my_full_q_list): shifted_mat = np.roll(giwk.mat, [-i for i in q], axis=(0, 1, 2)) @@ -591,10 +624,11 @@ def calculate_sigma_from_kernel_cpu( my_full_q_list: np.ndarray, ) -> SelfEnergy: r""" - Returns :math:`\Sigma_{ij}^{k} = -\frac{1}{2\beta N_q} \sum_q U^q_{r;aibc} K_{r;cbjd}^{q\nu} G_{ad}^{\omega-\nu}`. + Returns :math:`\Sigma_{12}^{k\nu} = -\frac{1}{2\beta N_q} \sum_{q\omega} \sum_{abcd} U^q_{r;a1bc} K_{r;cb2d}^{q\omega\nu} G_{ad}^{\nu-\omega}`. For very large momentum grids, this function is the slowest part compared to the rest of the code due to the repeated loops. There is no real way to speed it up further without leveraging GPUs or other hardware accelerators. - This is the CPU implementation (Fortran-ordered buffers, preallocated accumulator). + This is the CPU implementation (Fortran-ordered buffers, preallocated accumulator). Currently unused, see + :func:`calculate_sigma_from_kernel`. :param kernel: The self-energy kernel :math:`K` (full BZ, scattered across ranks). :param giwk: The momentum-dependent :class:`GreensFunction`. @@ -643,9 +677,10 @@ def calculate_sigma_from_kernel_gpu( my_full_q_list: np.ndarray, ) -> SelfEnergy: r""" - Returns :math:`\Sigma_{ij}^{k} = -\frac{1}{2\beta N_q} \sum_q U^q_{r;aibc} K_{r;cbjd}^{q\nu} G_{ad}^{\omega-\nu}`. + Returns :math:`\Sigma_{12}^{k\nu} = -\frac{1}{2\beta N_q} \sum_{q\omega} \sum_{abcd} U^q_{r;a1bc} K_{r;cb2d}^{q\omega\nu} G_{ad}^{\nu-\omega}`. For very large momentum grids, this function is the slowest part compared to the rest of the code due to the - repeated loops. This is the GPU implementation using CuPy. + repeated loops. This is the GPU implementation using CuPy. Currently unused, see + :func:`calculate_sigma_from_kernel`. :param kernel: The self-energy kernel :math:`K` (full BZ, scattered across ranks). :param giwk: The momentum-dependent :class:`GreensFunction`. @@ -693,7 +728,8 @@ def calculate_sigma_from_kernel_auto( r""" Dispatches the q-loop self-energy contraction to the GPU (:func:`calculate_sigma_from_kernel_gpu`) when CuPy and a usable CUDA device are available (one GPU per MPI rank, round-robin), otherwise falls back to the CPU - implementation (:func:`calculate_sigma_from_kernel_cpu`). + implementation (:func:`calculate_sigma_from_kernel_cpu`). Currently unused, see + :func:`calculate_sigma_from_kernel`. :param mpi_distributor: MPI distributor used to choose the per-rank GPU (see :class:`MpiDistributor`). :param kernel: The self-energy kernel :math:`K` (full BZ, scattered across ranks). @@ -867,13 +903,11 @@ def select_sigma_fft_device(mpi_distributor: MpiDistributor) -> bool: :param mpi_distributor: MPI distributor providing the per-rank GPU choice. :return: True if the GPU implementation should be used, False to fall back to the CPU implementation. """ - cp = None try: import cupy as cp except ImportError: return False # CuPy not installed -> CPU - n_gpus = 0 try: n_gpus = cp.cuda.runtime.getDeviceCount() except cp.cuda.runtime.CUDARuntimeError: @@ -929,6 +963,51 @@ def _map_kernel_to_full_bz( return mpi_utils.exchange_and_map_irrbz_fullbz(kernel, mpi_dist_irrk, mpi_dist_fullbz) +def _run_fft_sde_pass( + kernel_src: FourPoint, + mpi_dist_irrk: MpiDistributor, + mpi_dist_fullbz: MpiDistributor, + giwk_full: GreensFunction, + niw_index_w_pairs: list[tuple[int, int]], + use_gpu: bool, + negative_w: bool, +) -> SelfEnergy: + r""" + Runs one bosonic-frequency FFT self-energy pass: maps the (small) irreducible-BZ kernel to the full BZ + (consuming ``kernel_src``), optionally builds its time-reversed negative-:math:`\omega` block, contracts the + requested ``niw_index_w_pairs`` via :func:`calculate_sigma_from_kernel_fft`, and frees the full-BZ kernel. Both + passes of :func:`calculate_self_energy_q` go through this helper: the caller hands the positive pass a + :meth:`~dgamore.n_point_base.IHaveMat.copy` of the irreducible kernel (so it survives for the negative pass) and + the negative pass the original (which this consumes), so only a single full-BZ niw half is ever resident. + + :param kernel_src: The irreducible-BZ kernel for this pass; consumed by the full-BZ map (mutated or replaced). + :param mpi_dist_irrk: MPI distributor over the irreducible BZ q-points. + :param mpi_dist_fullbz: MPI distributor over the full BZ q-points. + :param giwk_full: The momentum-dependent :class:`GreensFunction`. + :param niw_index_w_pairs: The ``(kernel_w_index, w)`` pairs to contract (see + :func:`calculate_sigma_from_kernel_fft_cpu`). + :param use_gpu: Whether to run the GPU implementation (as decided by :func:`select_sigma_fft_device`). + :param negative_w: If True, build the negative-:math:`\omega` block via + :meth:`LocalNPoint.to_negative_niw_range` before contracting (the negative pass) and trim the kernel peak + back to the OS on free; if False, contract the mapped kernel directly (the positive pass). + :return: The rank-local R-space :class:`SelfEnergy` of this pass. + """ + # ``_map_kernel_to_full_bz`` may mutate-and-return its argument or return a fresh object, so free the source + # only when a distinct object came back. + kernel_full = _map_kernel_to_full_bz(kernel_src, mpi_dist_irrk, mpi_dist_fullbz) + if kernel_full is not kernel_src: + kernel_src.free() + + if negative_w: + kernel_neg = kernel_full.to_negative_niw_range() + kernel_full.free() # release the full-BZ positive copy as soon as the negative block is built + kernel_full = kernel_neg + + sigma = calculate_sigma_from_kernel_fft(mpi_dist_irrk, kernel_full, giwk_full, niw_index_w_pairs, use_gpu) + kernel_full.free(trim=negative_w) # coarse per-iteration trim on the last (negative) pass + return sigma + + def get_starting_sigma(default_sigma: SelfEnergy) -> tuple[SelfEnergy, int]: """ Tries to retrieve the last calculated self-energy from a previous self-consistency calculation as a starting point @@ -1043,6 +1122,104 @@ def _get_top_n_files(path: str, pattern: str, regex: re.Pattern) -> list[tuple[i return sigmas +def _build_giwk_full(comm: MPI.Comm, sigma: SelfEnergy, mu: float, ek: np.ndarray, beta: float) -> tuple: + r""" + Builds the full-grid Green's function :math:`G(k, \nu)`, optionally deduplicated across the MPI ranks that share + a physical node. With ``config.memory.use_shared_memory_giwk`` set (the default), the Dyson inversion runs only on + each node's root rank and the result is placed in one MPI shared-memory window per node, so ``giwk_full`` occupies + a single physical buffer per node instead of one private copy per rank (see + :func:`dgamore.mpi_utils.build_node_shared_array`). Otherwise every rank builds its own copy. The node topology is + discovered at runtime via ``comm.Split_type(MPI.COMM_TYPE_SHARED)`` (nothing about the cluster is hard-coded). + + :param comm: The (world) MPI communicator. + :param sigma: The self-energy :math:`\Sigma` entering the Dyson equation. + :param mu: Chemical potential :math:`\mu`. + :param ek: Band dispersion :math:`\varepsilon(k)`. + :param beta: Inverse temperature :math:`\beta`. + :return: The tuple ``(giwk_full, win, node_comm)``; ``win`` and ``node_comm`` are ``None`` on the non-shared path + and must otherwise be released with :func:`_release_shared_giwk` once ``giwk_full`` has been cut to its private + core box (the shared buffer is read-only and must not be freed while any rank still reads it). + """ + if not config.memory.use_shared_memory_giwk: + return GreensFunction.get_g_full(sigma, mu, ek, beta), None, None + + node_comm = comm.Split_type(MPI.COMM_TYPE_SHARED) + giwk_mat, win = mpi_utils.build_node_shared_array( + node_comm, lambda: GreensFunction.get_g_full(sigma, mu, ek, beta).mat + ) + giwk_full = GreensFunction( + giwk_mat, sigma, ek, sigma.full_niv_range, False, False, nk=ek.shape[:3], beta=beta, mu=mu + ) + return giwk_full, win, node_comm + + +def _release_shared_giwk(win, node_comm) -> None: + r""" + Releases the shared-memory window and node communicator allocated by :func:`_build_giwk_full`, once all node ranks + have finished reading ``giwk_full`` (i.e. after it has been cut to a private copy). The barrier guarantees no rank + is still reading the shared buffer when it is freed. A no-op when node-sharing was not used. + + :param win: The MPI shared-memory window (or ``None``). + :param node_comm: The node-local communicator (or ``None``). + :return: None. + """ + if node_comm is None: + return + node_comm.Barrier() + if win is not None: + win.Free() + node_comm.Free() + + +def _cut_and_reshare_giwk(giwk_full: GreensFunction, win, node_comm, niv: int) -> tuple: + r""" + Cuts ``giwk_full`` to the :math:`[-niv, niv)` core box. When ``giwk_full`` is node-shared (``node_comm`` is not + ``None``), the node root cuts the shared full-niv Green's function into a **new, smaller per-node shared window** + and every rank maps that; the caller then frees the old (large) full-niv window via :func:`_free_shared_window`. + This keeps the deduplicated ``giwk_full`` at one copy per node through the whole self-energy step, not just the + bubble. Without sharing it is a plain per-rank cut. + + :param giwk_full: The full-niv Green's function (possibly backed by a shared window). + :param win: The shared-memory window backing ``giwk_full`` (unused here; freed by the caller afterwards). + :param node_comm: The node-local communicator (or ``None`` on the non-shared path). + :param niv: Half width of the target fermionic core box. + :return: The tuple ``(giwk_cut, cut_win)``; ``cut_win`` is ``None`` on the non-shared or single-rank-node path. + """ + if node_comm is None: + return giwk_full.cut_niv(niv), None + + node_comm.Barrier() # every rank has finished reading the full-niv window (the bubble) + cut_mat, cut_win = mpi_utils.build_node_shared_array(node_comm, lambda: giwk_full.cut_niv(niv).mat) + giwk_cut = GreensFunction( + cut_mat, + giwk_full._sigma, + giwk_full._ek, + giwk_full.full_niv_range, + False, + False, + nk=giwk_full._ek.shape[:3], + beta=giwk_full._beta, + mu=giwk_full._mu, + ) + return giwk_cut, cut_win + + +def _free_shared_window(win, node_comm) -> None: + r""" + Frees a shared-memory window while keeping its node communicator alive - the communicator is reused for the cut + ``giwk_full`` window and released later by :func:`_release_shared_giwk`. A barrier guarantees no rank is still + reading the window's buffer. A no-op when there is no window. + + :param win: The MPI shared-memory window (or ``None``). + :param node_comm: The node-local communicator (or ``None``). + :return: None. + """ + if node_comm is None or win is None: + return + node_comm.Barrier() + win.Free() + + def calculate_self_energy_q( comm: MPI.Comm, u_loc: LocalInteraction, v_nonloc: Interaction, sigma_dmft: SelfEnergy, sigma_local: SelfEnergy ) -> SelfEnergy: @@ -1056,7 +1233,7 @@ def calculate_self_energy_q( :param u_loc: The bare local interaction :math:`U`. :param v_nonloc: The non-local interaction :math:`V^{q}`. :param sigma_dmft: The DMFT self-energy (used as the starting point and for the shell/tail correction). - :param sigma_local: The locally recomputed self-energy (used for the double-counting :math:`\Delta\Sigma`). + :param sigma_local: The locally recomputed self-energy (used for smoothing out the DGA :class:`SelfEnergy`). :return: The converged (or last-iteration) momentum-dependent DGA :class:`SelfEnergy`. """ logger = config.logger @@ -1086,7 +1263,7 @@ def calculate_self_energy_q( mu_history = _init_mu_history(starting_iter) niv_cut = min(config.box.niw_core + config.box.niv_full + 10, config.box.niv_dmft) - sigma_dmft_full = deepcopy(sigma_dmft) + sigma_dmft_full = sigma_dmft.copy() if comm.rank == 0: giwk_full_dmft = GreensFunction.get_g_full( @@ -1115,7 +1292,7 @@ def calculate_self_energy_q( delta_sigma = sigma_dmft.cut_niv(config.box.niv_core) - sigma_local.cut_niv(config.box.niv_core) - v_nonloc_full = deepcopy(v_nonloc) + v_nonloc_full = v_nonloc.copy() v_nonloc = v_nonloc.reduce_q(my_irr_q_list) for current_iter in range(starting_iter + 1, starting_iter + config.self_consistency.max_iter + 1): @@ -1127,8 +1304,8 @@ def calculate_self_energy_q( fock = mpi_dist_fullbz.allreduce(fock) logger.info("Calculated Hartree and Fock terms.") - giwk_full = GreensFunction.get_g_full( - sigma_old, mu_history[-1], config.lattice.hamiltonian.get_ek(), config.sys.beta + giwk_full, giwk_win, giwk_node_comm = _build_giwk_full( + comm, sigma_old, mu_history[-1], config.lattice.hamiltonian.get_ek(), config.sys.beta ) logger.log_memory_usage("giwk", giwk_full, comm.size) @@ -1159,27 +1336,31 @@ def calculate_self_energy_q( gchi0_q.save(name=f"gchi0_q_rank_{comm.rank}", output_dir=config.output.output_path) logger.log_memory_usage("Gchi0_q_full", gchi0_q, comm.size) - giwk_full = giwk_full.cut_niv(config.box.niv_core + config.box.niw_core) + # Cut giwk to the core box for the self-energy step. When node-shared, the node root cuts into a new, smaller + # per-node window and the large full-niv window is freed; the cut giwk stays one copy per node through the SDE. + old_giwk_win = giwk_win + giwk_full, giwk_win = _cut_and_reshare_giwk( + giwk_full, giwk_win, giwk_node_comm, config.box.niv_core + config.box.niw_core + ) + _free_shared_window(old_giwk_win, giwk_node_comm) - # sigma_old is not read again until the mixing step at the end of the iteration, and its shell - # [niv_core, niv_cut) is always the (k-independent) DMFT self-energy. Shrink the full-grid sigma_old to its core - # here -- freeing the broadcast shell through the kernel/SDE/fq peak -- and reconstruct the shell from sigma_dmft - # just before mixing, which restores it to niv_cut bit-for-bit. sigma_old = sigma_old.cut_niv(config.box.niv_core) - f_dc_loc = 2 * LocalFourPoint.load(os.path.join(config.output.output_path, "f_magn_loc.npy")).permute_orbitals( - "abcd->cbad" + f_dc_loc = ( + LocalFourPoint.load(os.path.join(config.output.output_path, "f_magn_loc.npy")) + .permute_orbitals("abcd->cbad") + .scale(2.0) ) - kernel = -calculate_sigma_dc_kernel(f_dc_loc, gchi0_q, u_loc) + kernel = calculate_sigma_dc_kernel(f_dc_loc, gchi0_q, u_loc).scale(-1.0) f_dc_loc.free() logger.info("Calculated double-counting kernel.") - gchi0_q_full_sum = 1.0 / config.sys.beta * gchi0_q.sum_over_all_vn(config.sys.beta) + gchi0_q_full_sum = gchi0_q.sum_over_all_vn(config.sys.beta).scale(1.0 / config.sys.beta) gchi0_q_core = gchi0_q.cut_niv(config.box.niv_core) gchi0_q.free() logger.log_memory_usage("Gchi0_q_core", gchi0_q_core, comm.size) - gchi0_q_core_inv = deepcopy(gchi0_q_core).invert(False) + gchi0_q_core_inv = gchi0_q_core.copy().invert(False) logger.log_memory_usage("Gchi0_q_inv", gchi0_q_core_inv, comm.size) if current_iter == 1: @@ -1188,14 +1369,17 @@ def calculate_self_energy_q( if config.eliashberg.perform_eliashberg: gchi0_q_core_inv.save(name=f"gchi0_q_inv_rank_{comm.rank}", output_dir=config.output.eliashberg_path) - gchi0_q_core_sum = 1.0 / config.sys.beta * gchi0_q_core.sum_over_all_vn(config.sys.beta) + gchi0_q_core_sum = gchi0_q_core.sum_over_all_vn(config.sys.beta).scale(1.0 / config.sys.beta) gchi0_q_core.free() gamma_dens = LocalFourPoint.load( os.path.join(config.output.output_path, "gamma_dens_loc.npy"), SpinChannel.DENS ) - kernel += calculate_sigma_kernel_r_q( - gamma_dens, gchi0_q_core_inv, gchi0_q_full_sum, gchi0_q_core_sum, u_loc, v_nonloc, mpi_dist_irrk + kernel.add( + calculate_sigma_kernel_r_q( + gamma_dens, gchi0_q_core_inv, gchi0_q_full_sum, gchi0_q_core_sum, u_loc, v_nonloc, mpi_dist_irrk + ), + copy=False, ) gamma_dens.free() mpi_dist_irrk.barrier() @@ -1204,8 +1388,11 @@ def calculate_self_energy_q( gamma_magn = LocalFourPoint.load( os.path.join(config.output.output_path, "gamma_magn_loc.npy"), SpinChannel.MAGN ) - kernel += 3 * calculate_sigma_kernel_r_q( - gamma_magn, gchi0_q_core_inv, gchi0_q_full_sum, gchi0_q_core_sum, u_loc, v_nonloc, mpi_dist_irrk + kernel.add( + calculate_sigma_kernel_r_q( + gamma_magn, gchi0_q_core_inv, gchi0_q_full_sum, gchi0_q_core_sum, u_loc, v_nonloc, mpi_dist_irrk + ).scale(3.0), + copy=False, ) gchi0_q_core_inv.free() gchi0_q_full_sum.free() @@ -1213,66 +1400,52 @@ def calculate_self_energy_q( gamma_magn.free() logger.info("Calculated kernel for magnetic channel.") - logger.info("Started calculation of DGA self-energy.") - - if config.memory.save_memory_for_sde: - # q-loop path: the kernel is mapped to the full BZ once (the full niw range is handled inside the q-loop). - kernel = _map_kernel_to_full_bz(kernel, mpi_dist_irrk, mpi_dist_fullbz) - logger.info("Kernel mapped to full BZ.") - sigma_new = calculate_sigma_from_kernel_auto(mpi_dist_fullbz, kernel, giwk_full, my_full_q_list) - kernel.free(trim=True) # coarse per-iteration trim: return the full-BZ kernel peak to the OS - sigma_new.mat = mpi_dist_fullbz.allreduce(sigma_new.mat) - else: - # FFT path: split the bosonic-frequency sum into a positive- and a negative-w pass so the full-BZ - # kernel is only ever materialized over half the niw range AND only one niw half exists at a time. The - # small irreducible-BZ kernel is kept and mapped to the full BZ separately for each pass; the negative - # block's time-reversal is applied *after* the irreducible->full-BZ unfold (not before). The unfold applies - # a per-k orbital rotation U (complex for multi-band) plus an optional conjugation (see - # :meth:`IAmNonLocal._map_to_full_bz`), and ``to_negative_niw_range`` conjugates the data, so it must see - # the already-unfolded kernel (conj of U*..U^T), exactly as the q-loop path and the original single-pass - # implementation did. Flipping the irreducible-BZ kernel first would leave U un-conjugated and corrupt - # sigma on the symmetry-folded q-points. - niw = config.box.niw_core - kernel_irr = kernel # the (small) irreducible-BZ positive-w kernel, mapped to the full BZ once per pass - # Decide CPU/GPU (and select the GPU) once, so the detection is not logged for each of the two passes. - use_gpu = select_sigma_fft_device(mpi_dist_fullbz) - - # positive pass: map a copy of the irr-BZ kernel to the full BZ and contract w = 0..+niw. The deep copy - # keeps ``kernel_irr`` (small) intact for the negative pass, which maps it again -- so only a single - # full-BZ niw half is ever resident. ``_map_kernel_to_full_bz`` may either mutate-and-return its argument - # or return a fresh object, so free the source copy only when a distinct object came back. - kernel_pos_irr = deepcopy(kernel_irr) - kernel_pos = _map_kernel_to_full_bz(kernel_pos_irr, mpi_dist_irrk, mpi_dist_fullbz) - if kernel_pos is not kernel_pos_irr: - kernel_pos_irr.free() - sigma_new = calculate_sigma_from_kernel_fft( - mpi_dist_irrk, kernel_pos, giwk_full, [(i, i) for i in range(niw + 1)], use_gpu - ) - kernel_pos.free() - - # negative pass: map the irr-BZ kernel to the full BZ, time-reverse the full-BZ result (w = 0, -1, ..., - # -niw), and contract w = -1..-niw (skip the w=0 duplicate). - kernel_neg_full = _map_kernel_to_full_bz(kernel_irr, mpi_dist_irrk, mpi_dist_fullbz) - if kernel_neg_full is not kernel_irr: - kernel_irr.free() # the irr-BZ kernel is no longer needed once the full-BZ negative source exists - kernel_neg = kernel_neg_full.to_negative_niw_range() - kernel_neg_full.free() # release the full-BZ positive copy as soon as the negative block is built - sigma_neg = calculate_sigma_from_kernel_fft( - mpi_dist_irrk, kernel_neg, giwk_full, [(i, -i) for i in range(1, niw + 1)], use_gpu - ) - kernel_neg.free(trim=True) # coarse per-iteration trim: return the full-BZ kernel peak to the OS + logger.info("Starting calculation of DGA self-energy.") + + # FFT contraction (the only production path - the q-loop variant peaks HIGHER in memory, see + # calculate_sigma_from_kernel): split the bosonic-frequency sum into a positive- and a negative-w pass so the + # full-BZ kernel is only ever materialized over half the niw range AND only one niw half exists at a time. + niw = config.box.niw_core + kernel_irr = kernel # the (small) irreducible-BZ positive-w kernel, mapped to the full BZ once per pass + # Decide CPU/GPU (and select the GPU) once + use_gpu = select_sigma_fft_device(mpi_dist_fullbz) + + sigma_new = _run_fft_sde_pass( + kernel_irr.copy(), + mpi_dist_irrk, + mpi_dist_fullbz, + giwk_full, + [(i, i) for i in range(niw + 1)], + use_gpu, + negative_w=False, + ) + sigma_neg = _run_fft_sde_pass( + kernel_irr, + mpi_dist_irrk, + mpi_dist_fullbz, + giwk_full, + [(i, -i) for i in range(1, niw + 1)], + use_gpu, + negative_w=True, + ) - sigma_new.mat += sigma_neg.mat # accumulate the rank-local R-space partial self-energies (in place) - sigma_neg.free() + sigma_new.mat += sigma_neg.mat # accumulate the rank-local R-space partial self-energies (in place) + sigma_neg.free() - sigma_new.mat = mpi_dist_fullbz.gather(sigma_new.mat) - if comm.rank == 0: - sigma_new = sigma_new.ifft().to_full_niv_range() - sigma_new = mpi_dist_fullbz.bcast_npoint(sigma_new) + sigma_new.mat = mpi_dist_fullbz.gather(sigma_new.mat) + if comm.rank == 0: + sigma_new = sigma_new.ifft().to_full_niv_range() + sigma_new = mpi_dist_fullbz.bcast_npoint(sigma_new) logger.info("Self-energy calculated from kernel.") logger.log_memory_usage("Non-local sigma", sigma_new, comm.size) + # giwk's momentum-space data is no longer needed (only its dispersion ek is used below); drop the shared view + # on every rank, then release the per-node cut-giwk window and its node communicator. + if giwk_win is not None: + giwk_full.mat = None + _release_shared_giwk(giwk_win, giwk_node_comm) + sigma_new = sigma_new + hartree + fock logger.info("Full non-local self-energy calculated.") @@ -1283,8 +1456,6 @@ def calculate_self_energy_q( # delta_sigma = sigma_dmft.cut_niv(config.box.niv_core) - sigma_new.q_mean().cut_niv(config.box.niv_core) logger.info("Applying mixing strategy to the self-energy.") - # Restore sigma_old's DMFT shell (cut after the bubble for memory) so the mixing and the residual below see the - # full niv_cut self-energy exactly as before. sigma_old = sigma_old.concatenate_self_energies(sigma_dmft) sigma_new = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter) @@ -1315,7 +1486,7 @@ def calculate_self_energy_q( logger.info(f"Updated mu from {old_mu} to {config.sys.mu}.") if comm.rank == 0: - sigma_occ = deepcopy(sigma_new).concatenate_self_energies(sigma_dmft_full) + sigma_occ = sigma_new.copy().concatenate_self_energies(sigma_dmft_full) giwk_occ = giwk_full.get_g_full( sigma_occ, config.sys.mu, config.lattice.hamiltonian.get_ek(), config.sys.beta ) diff --git a/dgamore/plotting.py b/dgamore/plotting.py index 5fd61325..4d972d6c 100644 --- a/dgamore/plotting.py +++ b/dgamore/plotting.py @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ -All matplotlib plotting helpers. These functions produce the diagnostic and result figures of a run — local +All matplotlib plotting helpers. These functions produce the diagnostic and result figures of a run - local self-energy / susceptibility checks, frequency-resolved four-point maps, momentum-space two-point maps (with optional Fermi-surface markers), the superconducting gap function, and the analytically continued spectral function along a high-symmetry path. Each routine saves and/or shows its figure. All plotting is gated behind diff --git a/dgamore/self_energy.py b/dgamore/self_energy.py index 58dbd801..5cb9850a 100644 --- a/dgamore/self_energy.py +++ b/dgamore/self_energy.py @@ -1,18 +1,17 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Single-particle self-energy. :class:`SelfEnergy` wraps the (possibly momentum-dependent) self-energy -:math:`\Sigma_{ab}(k, \nu)` and provides its high-frequency moments (:math:`\Sigma_\infty` and the +:math:`\Sigma_{12}(k, \nu)` and provides its high-frequency moments (:math:`\Sigma_\infty` and the :math:`1/\imath\nu` coefficient), tools to estimate the core frequency box, to append the analytic asymptotic tail beyond the core, to polynomial-fit the tail, and to interpolate the self-energy between temperatures. The moments are obtained by fitting the highest available Matsubara frequencies. """ import itertools as it -from copy import deepcopy import numpy as np from scipy.interpolate import PchipInterpolator, interp1d @@ -23,7 +22,7 @@ class SelfEnergy(TwoPoint): r""" - The (possibly momentum-dependent) single-particle self-energy :math:`\Sigma_{ab}(k, \nu)`. On top of the + The (possibly momentum-dependent) single-particle self-energy :math:`\Sigma_{12}(k, \nu)`. On top of the two-point orbital bookkeeping inherited from :class:`LocalTwoPoint` it provides the high-frequency moments (:math:`\Sigma_\infty` and the :math:`1/\imath\nu` coefficient), an estimate of the core frequency box, the construction/append of the analytic asymptotic tail beyond the core, a polynomial tail fit, and the @@ -118,14 +117,14 @@ def create_with_asympt_up_to_core(self) -> "SelfEnergy": asympt = self._get_asympt(niv=self.niv) if self._niv_core == self.niv: - return deepcopy(self) + return self.copy() if asympt.niv == 0: - return deepcopy(self) + return self.copy() copy = self.cut_niv(self._niv_core) if copy is self: # ``cut_niv`` was a no-op (``_niv_core >= self.niv``); deep-copy so the operation stays non-destructive. - copy = deepcopy(self) + copy = self.copy() copy.mat = np.concatenate( (asympt.mat[..., : asympt.niv - copy.niv], copy.mat, asympt.mat[..., asympt.niv + copy.niv :]), axis=-1 ) @@ -141,7 +140,7 @@ def append_asympt(self, niv: int): """ asympt = self._get_asympt(niv) if niv <= self.niv: - return deepcopy(self) + return self.copy() # ``np.concatenate`` allocates the result and only reads ``self.mat``, so clone the metadata without # duplicating the array first. copy = self._clone_without_mat() @@ -267,7 +266,7 @@ def fit_polynomial(self, n_fit: int = 4, degree: int = 3, niv_core: int = 0) -> :return: A new :class:`SelfEnergy` holding the polynomial fit, in the full fermionic frequency range. """ if n_fit == 0: - return deepcopy(self) + return self.copy() if n_fit > self.niv or n_fit < 0: n_fit = niv_core + 200 diff --git a/dgamore/symmetrize_new.py b/dgamore/symmetrize_new.py index a50cbf2d..339cbe4b 100644 --- a/dgamore/symmetrize_new.py +++ b/dgamore/symmetrize_new.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Standalone preprocessing script (a second installed console script) that converts a w2dynamics worm-sampled @@ -362,7 +362,7 @@ def complete(text, state): return None -if __name__ == "__main__": +def main(): default_filename = "Vertex.hdf5" default_output_filename = "g4iw_sym.hdf5" @@ -441,3 +441,7 @@ def complete(text, state): print(f"{len(ineq_numbers)} inequivalent atom(s) written to {output_filename}.") print("Done!") exit() + + +if __name__ == "__main__": + main() diff --git a/dgamore/symmetry_reduction.py b/dgamore/symmetry_reduction.py index 215349ec..e1d442b4 100644 --- a/dgamore/symmetry_reduction.py +++ b/dgamore/symmetry_reduction.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ @@ -58,7 +58,6 @@ import itertools import string - # ============================================================================ # Spatial ops on a discrete reciprocal grid # ============================================================================ @@ -475,7 +474,7 @@ def _discover_symmetries(H, atol, verbose=False): M_all = _enumerate_integer_matrices() M_candidates = [M for M in M_all if _M_preserves_grid(M, nk)] - # Dedupe M's by their grid action — when N_i = 1 for some axis, many M's + # Dedupe M's by their grid action - when N_i = 1 for some axis, many M's # produce the same k-grid index map. Use a tuple of (hash, length) plus # confirmation against stored representatives to avoid keeping nktot-sized # bytes for every distinct M (which costs ~nktot bytes per entry; for cubic @@ -566,7 +565,7 @@ def _canon_U_bytes(U): for conj in (False, True): # Quick dedup: if for this (idx_q, sigma, conj) we already have # an op, only one U is enough (the U is determined up to the - # group's commutant — finding more here is redundant for the IBZ). + # group's commutant - finding more here is redundant for the IBZ). # But we keep distinct U's because they're truly different group elts. if Hg is None: Hg = H_flat[idx_q].reshape(nx, ny, nz, norb, norb) @@ -952,7 +951,7 @@ def expand_tensor(T_ibz, kind="kb", sigma_power=1): # For every FBZ point k (in (nx,ny,nz) layout): # T_full(k) = sigma_k * U_k T(rep(k))^[*conj_k] U_k^dagger (per orbital index pair) # where rep(k) is given by pos_in_irrk[k_flat] -> position in irrk_ind. - "pos_in_irrk": pos_in_irrk, # shape (nktot,), int — irrk_inv equivalent + "pos_in_irrk": pos_in_irrk, # shape (nktot,), int - irrk_inv equivalent "Us": Us, # shape (nx, ny, nz, norb, norb), complex "sigmas": sigmas, # shape (nx, ny, nz), float (+/-1) "conjs": conjs, # shape (nx, ny, nz), bool @@ -971,15 +970,15 @@ def apply_auto_orbital_transform( axis enumerates k-points (or a contiguous slice thereof). The transformation follows the operator ordering - :math:`G_{abcd} := \langle T[c_a c^\dagger_b c_c c^\dagger_d]\rangle`, with annihilation indices (positions 1, 3) + :math:`G_{1234} := \langle T[c_1 c^\dagger_2 c_3 c^\dagger_4]\rangle`, with annihilation indices (positions 1, 3) transforming with :math:`U` and creation indices (positions 2, 4) with :math:`U^\dagger`, combined with :math:`\sigma` and conjugation: .. math:: - M_{ab}(k) &= \sigma_k\, U_{aa'} [M_{a'b'}(k_{\mathrm{rep}})]^{[*\mathrm{conj}_k]} U^\dagger_{b'b} \\ - M_{abcd}(k) &= \sigma_k^2\, U_{aa'} [M_{a'b'c'd'}(k_{\mathrm{rep}})]^{[*\mathrm{conj}_k]} U^\dagger_{b'b} - U_{cc'} U^\dagger_{d'd} + M_{12}(k) &= \sigma_k\, U_{1a} [M_{ab}(k_{\mathrm{rep}})]^{[*\mathrm{conj}_k]} U^\dagger_{b2} \\ + M_{1234}(k) &= \sigma_k^2\, U_{1a} [M_{abcd}(k_{\mathrm{rep}})]^{[*\mathrm{conj}_k]} U^\dagger_{b2} + U_{3c} U^\dagger_{d4} Since :math:`\sigma_k = \pm 1`, :math:`\sigma_k^2 = 1`; the 4-index case effectively has no sign factor, which is the correct physics for vertex quantities under particle-hole-like antisymmetries. diff --git a/dgamore/two_point.py b/dgamore/two_point.py index aa3f9303..94b16f42 100644 --- a/dgamore/two_point.py +++ b/dgamore/two_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems r""" Momentum-dependent two-point objects. :class:`TwoPoint` extends :class:`LocalTwoPoint` with one momentum axis @@ -11,8 +11,6 @@ :class:`LocalFourPoint`. """ -from copy import deepcopy - import numpy as np from dgamore.brillouin_zone import KGrid @@ -65,7 +63,7 @@ def permute_orbitals(self, permutation: str = "ab->ab") -> "TwoPoint": if split[0] == split[1]: return self - copy = deepcopy(self) + copy = self.copy() permutation = ( ( diff --git a/docs/conf.py b/docs/conf.py index ed1d6c64..feefc1d4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ # Native/optional dependencies that need not (and on a docs runner cannot easily) be installed to build the docs. # autodoc only needs to *import* the package; mocking these keeps the docs build light and runner-agnostic. -autodoc_mock_imports = ["mpi4py", "cupy"] +autodoc_mock_imports = ["mpi4py", "cupy", "psutil"] # Cross-reference the standard library and the scientific stack. intersphinx_mapping = { diff --git a/docs/configuration.rst b/docs/configuration.rst index 3c92c356..c9496d44 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -196,6 +196,7 @@ Superconducting properties are obtained by solving the linearised Eliashberg equ epsilon: 1e-6 # float symmetry: "random" # str include_local_part: True # bool + symmetrize_degenerate_gaps: True # bool subfolder_name: "Eliashberg" # str The equation is solved only when ``perform_eliashberg`` is ``True``. Enabling ``save_pairing_vertex`` or ``save_fq`` @@ -210,8 +211,10 @@ eigenvalues and the corresponding gap functions to an accuracy of ``epsilon``. T starting vector of the iteration: entering ``"d-wave"``, for example, begins from a gap function with d-wave symmetry, but ``"random"`` is sufficient most of the time. The pairing vertex includes local reducible diagrams by default, which can be skipped by setting ``include_local_part`` to ``False``; this is only advisable when s-wave symmetry is -not expected, as these diagrams become relevant in that case. Finally, the results are written to a subfolder named -according to ``subfolder_name``. +not expected, as these diagrams become relevant in that case. With ``symmetrize_degenerate_gaps`` enabled (the +default), gap functions belonging to (near-)degenerate eigenvalues are orthogonalized with a Loewdin scheme and +degenerate doublets are rotated to their mirror-adapted (:math:`p_x`/:math:`p_y`-like) partners. Finally, the +results are written to a subfolder named according to ``subfolder_name``. .. _self-energy-interpolation: @@ -278,30 +281,32 @@ Memory efficiency ----------------- For very large parameter sets memory becomes the main bottleneck, owing to the vectorised nature of the implemented -equations. This section therefore exposes more memory-efficient algorithms for five of the heaviest steps. +equations. This section therefore exposes more memory-efficient algorithms for four of the heaviest steps. .. code-block:: yaml memory: save_memory_for_chi0q: False # bool save_memory_for_chiq_aux: False # bool - save_memory_for_sde: False # bool save_memory_for_fq: False # bool save_memory_for_lanczos: False # bool - -These switches control, in turn, the construction of the bare bubble susceptibility, of the auxiliary -susceptibility entering the Schwinger-Dyson equation, of the Schwinger-Dyson equation itself, of the full ladder -vertices for the Eliashberg equation, and of the Lanczos algorithm. Enabling any of them increases the runtime -substantially because of the additional Python-level looping and MPI communication. Under the hood, the bubble and the -Schwinger-Dyson steps use fast Fourier transforms when their switches are left at ``False``; this gives by far the -largest speed-up while barely affecting the memory footprint, so it can be kept at the default in almost all cases. -The largest memory savings come from the auxiliary-susceptibility and full-ladder-vertex switches, which shrink -those objects considerably, whereas the Lanczos switch matters only for extremely large parameter sets and can -usually stay disabled. + use_shared_memory_giwk: True # bool + +The first four switches control, in turn, the construction of the bare bubble susceptibility, of the auxiliary +susceptibility entering the Schwinger-Dyson equation, of the full ladder vertices for the Eliashberg equation, and +of the Lanczos algorithm. Enabling any of them increases the runtime substantially because of the additional +Python-level looping and MPI communication. Under the hood, the bubble uses fast Fourier transforms when its switch +is left at ``False``; this gives by far the largest speed-up while barely affecting the memory footprint, so it can +be kept at the default in almost all cases. The Schwinger-Dyson equation itself has no switch: it always runs its +Fourier-transformed form, which processes the two bosonic frequency halves in separate passes and thereby needs no +more memory than the alternative momentum-loop formulation would. The largest memory savings come from the +auxiliary-susceptibility and full-ladder-vertex switches, which shrink those objects considerably, whereas the +Lanczos switch matters only for extremely large parameter sets and can usually stay disabled. In practice these switches rarely need to be set by hand. Before the heavy part of a run begins, DGAmore inspects -the memory available on every node together with an analytic estimate of the peak memory each of the five steps -consumes, accounting for how the momentum points are distributed across the MPI ranks that share a node. Whenever +the memory available on every node together with an analytic estimate of the peak memory each of the heavy steps +(including the switch-less Schwinger-Dyson contraction) consumes, accounting for how the momentum points are +distributed across the MPI ranks that share a node. Whenever the default, faster variant of a step would not fit, the corresponding switch is turned on automatically. The estimate is evaluated as a node total: it sums, over all ranks placed on a node, the data that each rank keeps resident throughout the calculation plus the transient peak of the step in question, and requires the result to stay @@ -309,7 +314,15 @@ below ninety percent of that node's available memory. Because the switches act p node decides whether a given switch is enabled. A switch that is explicitly set to ``True`` in the configuration is always honoured: the automatic detection can -only enable additional switches, never turn off one that was requested. Conversely, if even the most memory-frugal -variant of a required step does not fit on some node, the run stops immediately with a ``MemoryError`` that -recommends using more nodes, fewer ranks per node, or a smaller frequency box or momentum grid, rather than failing +only enable additional switches, never turn off one that was requested. Conversely, if the variant of a step that is +about to run does not fit on some node - because neither of its variants fits, or because a switch forced by the +configuration selects a variant that does not - the run stops immediately with a ``MemoryError`` that recommends +using more nodes, fewer ranks per node, or a smaller frequency box or momentum grid, rather than failing unpredictably partway through. + +The final switch, ``use_shared_memory_giwk``, is of a different kind and is enabled by default. The full-grid lattice +Green's function is identical on every rank, yet each rank would normally rebuild and store its own copy, so a node +running many ranks holds that (large) array many times over. When this switch is on, the ranks that share a physical +node instead keep a single copy in one MPI shared-memory window: the Dyson inversion is performed only by that node's +first rank and the others map the same buffer read-only. The node topology is discovered automatically at runtime, so +nothing about the cluster needs to be configured. diff --git a/docs/contributing.rst b/docs/contributing.rst index e9a02786..4f49f050 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -81,16 +81,18 @@ When you would like to contribute code, the following workflow keeps things smoo pytest tests # fast suite (skips tests marked slow) pytest tests --runslow # full suite, as run in CI - pytest tests --runslow --cov=dgamore --cov-report=term-missing # with coverage + pytest tests --runslow --cov=dgamore --cov-report=term-missing --cov-fail-under=85 # coverage, as CI runs it 5. **Open a pull request** against the ``main`` branch, with a short description of what you changed and why. If your pull request is related to an existing issue, mentioning it helps connect the two. -A continuous integration pipeline runs the full test suite on every pull request, across Python 3.12 to 3.14 on both -Linux and macOS. This is there to catch regressions, not to be a gatekeeper, so please do not worry if something turns -red on the first try; it is a normal part of the process, and we are glad to help you get it passing. A coverage tool -also checks that the overall test coverage stays above eighty-five percent, so adding tests for your changes is the -best way to keep it healthy. +A continuous integration pipeline runs on every pull request. It checks that the code is Black-formatted, then runs the +full test suite across Python 3.12 to 3.14 on both Linux and macOS. This is there to catch regressions, not to be a +gatekeeper, so please do not worry if something turns red on the first try; it is a normal part of the process, and we +are glad to help you get it passing. The pipeline also requires the overall test coverage to stay at **at least +eighty-five percent**, and the build fails if it drops below that threshold. Beyond the overall figure, the new or +changed code in a pull request (the *patch*) must itself be covered to **at least eighty-five percent**, so please add +tests for what you write rather than relying on the rest of the code base to carry the average. Coding style ------------ diff --git a/docs/installation.rst b/docs/installation.rst index 732769f0..a91947f4 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -47,8 +47,8 @@ The editable install is recommended for development, as it lets you modify the s pick up upstream changes simply by pulling the latest version. Please make sure no older cached versions of the dependencies are reused, as this can lead to installation problems. -Installing the package also places the entry point ``DGAmore.py`` on your ``PATH``, so you can launch a run from -anywhere without specifying its full path. To verify the installation, you can optionally install ``pytest`` and run +Installing the package also places the entry points ``DGAmore`` and ``symmetrize`` on your ``PATH``, so you can +launch a run from anywhere without specifying its full path. To verify the installation, you can optionally install ``pytest`` and run the test suite: .. code-block:: bash diff --git a/docs/usage.rst b/docs/usage.rst index 1503ec3f..88312691 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,7 +7,7 @@ Preparing the input DGAmore takes as input the result of a DMFT calculation, which currently must be produced with w2dynamics, a continuous-time quantum Monte Carlo solver based on the hybridisation expansion. A w2dynamics run yields both the one-particle quantities and a two-particle output containing the four-point Green's functions. The two-particle -output first has to be converted into the format DGAmore expects. This is done by running the ``symmetrize_new.py`` +output first has to be converted into the format DGAmore expects. This is done by running the ``symmetrize`` script, which is installed alongside the main entry point; it prompts for the input and output file names and writes only the density and magnetic spin components of the two-particle Green's function to the output file. The original, unsymmetrised vertex file is not needed afterwards. @@ -20,19 +20,19 @@ additional file specifying the local and non-local interaction can be provided. Running a calculation --------------------- -The main entry point of the program is ``DGAmore.py``. Because it is added to the Python environment as a standalone +The main entry point of the program is ``DGAmore``. Because it is added to the Python environment as a standalone executable, it can be invoked by name without its full path. For single-core execution, which is mainly intended for testing, run: .. code-block:: bash - DGAmore.py + DGAmore For a parallel run with MPI, use: .. code-block:: bash - mpiexec -np DGAmore.py + mpiexec -np DGAmore Instead of ``mpiexec`` you may also use ``mpirun`` or, on SLURM-based clusters, ``srun``. The number of processes ```` should be chosen according to the problem size and the available resources; note that increasing the @@ -46,7 +46,7 @@ processes and loads the configuration file ``my_config.yaml`` from ``/configs/`` .. code-block:: bash - mpiexec -np 8 DGAmore.py -p /configs/ -c my_config.yaml + mpiexec -np 8 DGAmore -p /configs/ -c my_config.yaml On a SLURM-based cluster, a typical job submission script looks as follows: @@ -71,10 +71,10 @@ On a SLURM-based cluster, a typical job submission script looks as follows: export OMP_NUM_THREADS=1 # Recommended on SLURM-based clusters: - srun DGAmore.py -p "" -c ".yaml" + srun DGAmore -p "" -c ".yaml" # Alternatively, with mpirun or mpiexec: - mpirun -np $SLURM_NTASKS DGAmore.py -p "" -c ".yaml" + mpirun -np $SLURM_NTASKS DGAmore -p "" -c ".yaml" The ``-o`` and ``-e`` options set the files for the job output and errors; here both are written to the same file, but separate files may be used instead. The results of a completed run are written to a subdirectory of the output diff --git a/pyproject.toml b/pyproject.toml index 11f37678..b66bdcc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,14 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.black] -line-length = 120 \ No newline at end of file +line-length = 120 + +[tool.coverage.run] +omit = [ + "*/DGAmore.py", + "*/plotting.py", + "*/config_parser.py", + "*/symmetrize_new.py", + "*/ana_cont.py", + "*/dmft_interface.py", +] \ No newline at end of file diff --git a/setup.py b/setup.py index 886e28cb..3b8a465e 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,11 @@ "Operating System :: POSIX", "Operating System :: POSIX :: Linux", ], - scripts=["dgamore/DGAmore.py", "dgamore/symmetrize_new.py"], + entry_points={ + "console_scripts": [ + "DGAmore = dgamore.DGAmore:main", + "symmetrize = dgamore.symmetrize_new:main", + ], + }, python_requires=">=3.12", ) diff --git a/tests/__init__.py b/tests/__init__.py index 09901cb1..5c1a04f0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,5 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems - diff --git a/tests/conftest.py b/tests/conftest.py index eed89f4c..e0f271e5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import logging @@ -179,8 +179,10 @@ def allreduce_numpy(sendbuf, recvbuf, op=None): comm_mock.Bcast.side_effect = bcast_numpy comm_mock.Allreduce.side_effect = allreduce_numpy - # Split should return itself + # Split / Split_type return the single-rank comm itself; Free is a no-op comm_mock.Split.return_value = comm_mock + comm_mock.Split_type.return_value = comm_mock + comm_mock.Free.return_value = None return comm_mock @@ -235,15 +237,17 @@ def _assign(buf, data): class Comm: - def __init__(self, size=1): - self._size = int(size) + def __init__(self, size=1, members=None): + self._members = tuple(members) if members is not None else tuple(range(int(size))) + self._size = len(self._members) self._barrier = _threading.Barrier(self._size) if self._size > 1 else None self._inboxes = [dict() for _ in range(self._size)] self._lock = _threading.Lock() self._store = [None] * self._size + self._split_cache = {} def Get_rank(self): - return getattr(_tls, "rank", 0) + return self._members.index(getattr(_tls, "rank", 0)) def Get_size(self): return self._size @@ -265,6 +269,21 @@ def abort_barrier(self): if self._barrier is not None: self._barrier.abort() + def Split_type(self, split_type, key=0, info=None): + # Group the ranks by hostname (COMM_TYPE_SHARED -> one sub-communicator per node). Threads share memory, so + # same-node ranks receive the SAME cached Comm instance and therefore a shared barrier/store, which is what + # makes the shared-memory window below genuinely shared. + host = getattr(_tls, "hostname", "node0") + hosts = list(self._collective(host)) + group = tuple(self._members[i] for i in range(self._size) if hosts[i] == host) + with self._lock: + if host not in self._split_cache: + self._split_cache[host] = Comm(members=group) + return self._split_cache[host] + + def Free(self): + pass + def _collective(self, contribution): r = self.rank self._bw() @@ -356,13 +375,33 @@ def Irecv(self, buf, source, tag=0): return _Request(lambda: _assign(buf, self._queue(self.rank, (source, tag)).get(timeout=_QUEUE_TIMEOUT))) +class Win: + # Minimal MPI shared-memory window over the threaded fake Comm. Because threads share an address space, the node + # root (local rank 0) allocates one bytearray and every rank of the (node) communicator receives the SAME object + # via bcast, so a numpy view over it is genuinely shared - writes by the root are visible to all. + @staticmethod + def Allocate_shared(size, disp_unit=1, info=None, comm=None): + win = Win() + buf = bytearray(int(size)) if comm.Get_rank() == 0 else None + win._buf = comm.bcast(buf, root=0) + return win + + def Shared_query(self, rank): + return self._buf, 1 + + def Free(self): + self._buf = None + + COMM_WORLD = Comm(1) FAKE_MPI = _types.SimpleNamespace( Comm=Comm, Request=Request, + Win=Win, IN_PLACE=IN_PLACE, REQUEST_NULL=REQUEST_NULL, + COMM_TYPE_SHARED=1, COMM_WORLD=COMM_WORLD, Get_processor_name=Get_processor_name, ) diff --git a/tests/test_autodetect_memory.py b/tests/test_autodetect_memory.py index 73fd9cc0..b7571056 100644 --- a/tests/test_autodetect_memory.py +++ b/tests/test_autodetect_memory.py @@ -9,7 +9,7 @@ import dgamore.config as config import dgamore.DGAmore as dgamore_main -from dgamore.memory_estimator import estimate_peaks +from dgamore.memory_estimator import BranchPeak, estimate_peaks from tests.conftest import create_comm_mock # the q-grid / box parameters the fake_system fixture installs, so tests can reproduce the driver's estimate @@ -28,7 +28,6 @@ def fake_system(monkeypatch): config.eliashberg.perform_eliashberg = False config.memory.save_memory_for_chi0q = False config.memory.save_memory_for_chiq_aux = False - config.memory.save_memory_for_sde = False config.memory.save_memory_for_fq = False config.memory.save_memory_for_lanczos = False config.lattice.q_grid = SimpleNamespace(nk_tot=FIXTURE_PARAMS["nk_tot"], nk_irr=FIXTURE_PARAMS["nk_irr"]) @@ -55,26 +54,34 @@ def _mock_comm(size=1, allgather=None): def _node_total(branch, which, r, with_eliashberg=False): - """Reproduces the driver's node total = r*(baseline+distributed) + single for one branch/path.""" - baseline, peaks = estimate_peaks(**FIXTURE_PARAMS, n_ranks=r, with_eliashberg=with_eliashberg) + """Reproduces the driver's node total incl. the shared-giwk credit for one branch/path on an r-rank node.""" + peaks = estimate_peaks(**FIXTURE_PARAMS, n_ranks=r, with_eliashberg=with_eliashberg) bp = peaks[branch] distributed = bp.off_distributed if which == "off" else bp.on_distributed single = bp.off_single if which == "off" else bp.on_single - return r * (baseline + distributed) + single + total = r * (bp.baseline + distributed) + single + if config.memory.use_shared_memory_giwk: + total -= (r - 1) * bp.giwk_shareable + return total def _all_node_totals(which, r, with_eliashberg): - _, peaks = estimate_peaks(**FIXTURE_PARAMS, n_ranks=r, with_eliashberg=with_eliashberg) + peaks = estimate_peaks(**FIXTURE_PARAMS, n_ranks=r, with_eliashberg=with_eliashberg) return [_node_total(k, which, r, with_eliashberg) for k in peaks] +def _mock_branch( + baseline=0.0, giwk_shareable=0.0, off_distributed=0.0, off_single=0.0, on_distributed=0.0, on_single=0.0 +): + return BranchPeak(baseline, giwk_shareable, off_distributed, off_single, on_distributed, on_single) + + def test_large_memory_keeps_all_flags_off(fake_system): """A large free-memory budget on a tiny problem leaves all lean flags off.""" fake_system(64 * 1024**3) # 64 GiB free, tiny problem -> nothing forced on dgamore_main.autodetect_memory_settings(_mock_comm()) assert config.memory.save_memory_for_chiq_aux is False assert config.memory.save_memory_for_chi0q is False - assert config.memory.save_memory_for_sde is False def test_tiny_memory_forces_lean_flags_on(fake_system): @@ -112,7 +119,7 @@ def test_more_ranks_per_node_raise_the_distributed_node_total(fake_system): config.memory.save_memory_for_chiq_aux = False one = _node_total("chiq_aux", "off", r=1) two = _node_total("chiq_aux", "off", r=2) - assert two > one # distributed block counted r times + assert two > one # distributed block counted r times (minus the one-copy giwk credit) budget = 0.5 * (one + two) # fits at 1 rank, overflows at 2 avail = int(budget / 0.9) @@ -153,11 +160,13 @@ def test_eliashberg_flag_forced_on_under_pressure(fake_system): assert config.memory.save_memory_for_fq is True -def test_autodetect_forwards_eliashberg_fq_flags(fake_system, monkeypatch): - """The driver forwards save_fq and construct_fq_cheap from config.eliashberg into estimate_peaks.""" +def test_autodetect_forwards_eliashberg_flags(fake_system, monkeypatch): + """The driver forwards save_fq, construct_fq_cheap, save_pairing_vertex and n_eig into estimate_peaks.""" config.eliashberg.perform_eliashberg = True monkeypatch.setattr(config.eliashberg, "save_fq", True, raising=False) monkeypatch.setattr(config.eliashberg, "construct_fq_cheap", True, raising=False) + monkeypatch.setattr(config.eliashberg, "save_pairing_vertex", True, raising=False) + monkeypatch.setattr(config.eliashberg, "n_eig", 3, raising=False) fake_system(64 * 1024**3) captured = {} @@ -171,3 +180,168 @@ def spy(**kwargs): dgamore_main.autodetect_memory_settings(_mock_comm()) assert captured["save_fq"] is True assert captured["construct_fq_cheap"] is True + assert captured["save_pairing_vertex"] is True + assert captured["n_eig"] == 3 + + +def test_memory_config_shares_giwk_by_default(): + """The node-shared giwk optimization is enabled by default (disable-able for the NUMA case).""" + assert config.MemoryConfig().use_shared_memory_giwk is True + + +def test_shared_giwk_credits_the_bubble_branch_node_total(fake_system, monkeypatch): + """use_shared_memory_giwk subtracts the deduplicated giwk copies from the chi0q (bubble) node total, so the fast + FFT bubble fits at a budget where the replicated estimate would force the lean path on.""" + fake_system(1) # installs the fixture config; the real budget is set per-branch below + r = 4 + giwk_half = 1024.0**2 + baseline = 2.0 * giwk_half + big = 50.0 * baseline # chi0q fast is a single-rank transient and the binding branch; its lean path is tiny + tiny = _mock_branch(baseline=baseline, giwk_shareable=giwk_half) + peaks = { + "chi0q": _mock_branch(baseline=baseline, giwk_shareable=giwk_half, off_single=big), + "chiq_aux": tiny, + "sde": tiny, + } + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + + uncredited = r * baseline + big # r*(baseline+off_distributed) + off_single, off_distributed == 0 + budget = uncredited - 0.5 * (r - 1) * giwk_half # between the credited (r->1 giwk) total and the uncredited one + ranks = lambda obj: [obj] * r + + config.memory.use_shared_memory_giwk = True + config.memory.save_memory_for_chi0q = False + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(budget / 0.9))) + dgamore_main.autodetect_memory_settings(_mock_comm(size=r, allgather=ranks)) + assert config.memory.save_memory_for_chi0q is False # credited fast bubble fits + + config.memory.use_shared_memory_giwk = False + config.memory.save_memory_for_chi0q = False + dgamore_main.autodetect_memory_settings(_mock_comm(size=r, allgather=ranks)) + assert config.memory.save_memory_for_chi0q is True # uncredited fast bubble overflows -> lean forced on + + +def test_shared_giwk_credits_a_non_bubble_sde_branch(fake_system, monkeypatch): + """The giwk credit applies to every SDE-section branch, not just the bubble: the heavy chiq_aux fast path fits + with sharing where the replicated estimate would force it onto the lean per-q path.""" + fake_system(1) + r = 4 + giwk_half = 1024.0**2 + baseline = 2.0 * giwk_half + big = 50.0 * baseline # chiq_aux fast is the binding branch (distributed); its lean path is trivially small here + tiny = _mock_branch(baseline=baseline, giwk_shareable=giwk_half) + peaks = { + "chi0q": tiny, + "chiq_aux": _mock_branch(baseline=baseline, giwk_shareable=giwk_half, off_distributed=big), + "sde": tiny, + } + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + + uncredited = r * (baseline + big) # off_single == 0 + budget = uncredited - 0.5 * (r - 1) * giwk_half # between the credited (giwk r->1) and the uncredited total + ranks = lambda obj: [obj] * r + + config.memory.use_shared_memory_giwk = True + config.memory.save_memory_for_chiq_aux = False + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(budget / 0.9))) + dgamore_main.autodetect_memory_settings(_mock_comm(size=r, allgather=ranks)) + assert config.memory.save_memory_for_chiq_aux is False # credited fast path fits + + config.memory.use_shared_memory_giwk = False + config.memory.save_memory_for_chiq_aux = False + dgamore_main.autodetect_memory_settings(_mock_comm(size=r, allgather=ranks)) + assert config.memory.save_memory_for_chiq_aux is True # uncredited fast path overflows -> lean forced on + + +def test_eliashberg_branch_baseline_gets_no_giwk_credit(fake_system, monkeypatch): + """A branch with giwk_shareable == 0 (the eliashberg ones run on the private per-rank giwk_dga) is not credited.""" + fake_system(1) + r = 4 + baseline = 2.0 * 1024.0**2 + peaks = { + "chi0q": _mock_branch(baseline=baseline, giwk_shareable=baseline / 2, off_single=baseline), + "fq": _mock_branch(baseline=baseline, off_distributed=baseline), + } + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + + # budget between the credited chi0q fast total (3.5 b) / the fq lean total (4 b) and the fq fast total (8 b); + # the uncredited chi0q fast total (5 b) would also overflow, so chi0q staying off shows the credit was applied. + budget = 4.5 * baseline + config.memory.use_shared_memory_giwk = True + config.memory.save_memory_for_chi0q = False + config.memory.save_memory_for_fq = False + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(budget / 0.9))) + dgamore_main.autodetect_memory_settings(_mock_comm(size=r, allgather=lambda obj: [obj] * r)) + assert config.memory.save_memory_for_chi0q is False # credited -> fits + assert config.memory.save_memory_for_fq is True # uncredited baseline + transient overflows -> lean forced on + + +def test_heavier_lean_path_does_not_raise_when_fast_fits(fake_system, monkeypatch): + """A branch whose lean path peaks higher than its fast one must not raise as long as the fast path fits and the + user did not force the lean flag.""" + fake_system(1) + small, big = 1024.0**2, 100 * 1024.0**2 + peaks = {"chiq_aux": _mock_branch(baseline=small, off_distributed=small, on_distributed=big)} + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(10 * small))) + + config.memory.save_memory_for_chiq_aux = False + dgamore_main.autodetect_memory_settings(_mock_comm()) + assert config.memory.save_memory_for_chiq_aux is False # fast path fits -> no raise despite the oversized lean path + + +def test_user_forced_lean_path_that_overflows_raises(fake_system, monkeypatch): + """A user-forced lean flag whose path overflows the node raises even though the fast path would fit.""" + fake_system(1) + small, big = 1024.0**2, 100 * 1024.0**2 + peaks = {"chiq_aux": _mock_branch(baseline=small, off_distributed=small, on_distributed=big)} + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(10 * small))) + + config.memory.save_memory_for_chiq_aux = True + with pytest.raises(MemoryError, match="fast path would fit"): + dgamore_main.autodetect_memory_settings(_mock_comm()) + + +def test_flagless_sde_step_is_verified_and_raises_on_overflow(fake_system, monkeypatch): + """The switch-less Schwinger-Dyson FFT contraction is still budget-checked: it passes silently when it fits and + raises a MemoryError naming the step when it overflows the node.""" + fake_system(1) + small, big = 1024.0**2, 100 * 1024.0**2 + peaks = {"sde": _mock_branch(baseline=small, off_distributed=big, on_distributed=big)} + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(10 * big))) + dgamore_main.autodetect_memory_settings(_mock_comm()) # fits -> no raise, no flag to set + + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(10 * small))) + with pytest.raises(MemoryError, match="Schwinger-Dyson"): + dgamore_main.autodetect_memory_settings(_mock_comm()) + + +def test_lanczos_single_rank_peak_doubled_on_single_node_multi_rank(fake_system, monkeypatch): + """On a single-node multi-rank job the singlet and triplet solves run concurrently on the same node, so the + lanczos fast-path single-rank peak is doubled; spread over two nodes each carries only one solver.""" + fake_system(1) + single = 10 * 1024.0**2 + peaks = {"lanczos": _mock_branch(baseline=1.0, off_single=single, on_distributed=1.0)} + monkeypatch.setattr(dgamore_main.memory_estimator, "estimate_peaks", lambda **kw: peaks) + budget = 1.5 * single # fits one solver per node, not two + monkeypatch.setattr(dgamore_main.psutil, "virtual_memory", lambda: SimpleNamespace(available=int(budget / 0.9))) + + config.memory.save_memory_for_lanczos = False + dgamore_main.autodetect_memory_settings(_mock_comm(size=2, allgather=lambda obj: [obj, obj])) + assert config.memory.save_memory_for_lanczos is True # both solvers on one node -> doubled peak overflows + + config.memory.save_memory_for_lanczos = False + two_nodes = lambda obj: [("node0", obj[1]), ("node1", obj[1])] + dgamore_main.autodetect_memory_settings(_mock_comm(size=2, allgather=two_nodes)) + assert config.memory.save_memory_for_lanczos is False # one solver per node -> fits + + +def test_dgamore_excludes_osc_ucx_before_mpi_init(): + """DGAmore defaults OMPI_MCA_osc to '^ucx' before importing mpi4py, muting the benign osc/ucx shared-window warning + with no user configuration; the ordering (set before MPI_Init) is what makes it take effect.""" + src = open(dgamore_main.__file__, encoding="utf-8").read() + assert 'os.environ.setdefault("OMPI_MCA_osc", "^ucx")' in src + assert src.index('os.environ.setdefault("OMPI_MCA_osc", "^ucx")') < src.index("from mpi4py import MPI") diff --git a/tests/test_brillouin_zone.py b/tests/test_brillouin_zone.py index 089f4246..42ae8f7a 100644 --- a/tests/test_brillouin_zone.py +++ b/tests/test_brillouin_zone.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems from unittest.mock import patch @@ -488,7 +488,7 @@ def test_k_axis_normalized_positions_and_length(): # distances between consecutive points: [1, 1, 1] -> cumulative [1,2,3] # k_axis_pos = [0,1,2,3] -> normalized by 3 -> [0, 1/3, 2/3, 1] expected = np.array([0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0]) - assert np.allclose(kp.k_axis, expected, rtol=1e-12, atol=1e-12) + assert np.allclose(kp.k_axis, expected, atol=1e-12) assert kp.k_axis.size == kp.nk_tot @@ -608,7 +608,7 @@ def test_get_lattice_symmetries_from_string_auto_is_case_insensitive(): def _make_small_real_cubic_h(nx=4, ny=4, nz=4, nb=1): - """A simple real, Hermitian, cubic-symmetric H on a small grid — convenient for + """A simple real, Hermitian, cubic-symmetric H on a small grid - convenient for testing auto-detection. With a single band the orbital action is trivial; the discovered symmetry is the spatial cubic group (8 ops for nx=ny=nz with all-axes inversions + permutations).""" diff --git a/tests/test_bubble_gen.py b/tests/test_bubble_gen.py index 923b7eb6..f96c6de7 100644 --- a/tests/test_bubble_gen.py +++ b/tests/test_bubble_gen.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Parity tests for :class:`dgamore.bubble_gen.BubbleGenerator`. Each test compares the produced bubble against an @@ -59,7 +59,7 @@ def test_create_generalized_chi0_matches_reference(): ref[a, b, c, d, iw, iv] = -beta * gl * gr assert res.mat.shape == ref.shape - assert np.allclose(res.mat, ref, atol=1e-4, rtol=1e-4) + assert np.allclose(res.mat, ref, atol=1e-4) def test_create_generalized_chi0_q_matches_reference(): @@ -90,7 +90,7 @@ def test_create_generalized_chi0_q_matches_reference(): assert res.mat.shape == ref.shape assert res.has_compressed_q_dimension is True - assert np.allclose(res.mat, ref, atol=1e-4, rtol=1e-4) + assert np.allclose(res.mat, ref, atol=1e-4) def test_create_generalized_chi0_q_no_copy_of_full_green_function(): @@ -125,7 +125,7 @@ def test_create_generalized_chi0_pp_w0_matches_reference(): assert res.mat.shape == ref.shape assert res.frequency_notation == FrequencyNotation.PP - assert np.allclose(res.mat, ref, atol=1e-4, rtol=1e-4) + assert np.allclose(res.mat, ref, atol=1e-4) def test_create_generalized_chi0_pp_w0_does_not_mutate_input(): @@ -153,12 +153,12 @@ def test_create_generalized_chi0_q_pp_w0_matches_reference(): for b in range(nb): for c in range(nb): for d in range(nb): - # G_ad^k * conj(G_cb^k) (transpose_orbitals -> g[c,b], conjugated) + # G_14^{kv} * conj(G_32^{kv}) (transpose_orbitals -> g[c,b], conjugated) ref[k, a, b, c, d, :] = gm[k, a, d, :] * np.conj(gm[k, c, b, :]) assert res.mat.shape == ref.shape assert res.frequency_notation == FrequencyNotation.PP - assert np.allclose(res.mat, ref, atol=1e-4, rtol=1e-4) + assert np.allclose(res.mat, ref, atol=1e-4) def test_create_generalized_chi0_q_pp_w0_does_not_mutate_input(): @@ -168,3 +168,25 @@ def test_create_generalized_chi0_q_pp_w0_does_not_mutate_input(): g_before = g.mat.copy() BubbleGenerator.create_generalized_chi0_q_pp_w0(g, niv_pp, bz.KGrid(nk, symmetries=[])) assert np.array_equal(g.mat, g_before) + + +def test_momentum_pp_bubble_is_local_pp_bubble_in_acbd_layout(): + """Momentum pp bubble equals the local pp bubble permuted 'abcd->acbd' (up to the -beta the momentum form omits).""" + nb, niv_pp, beta = 2, 3, 2.0 + nk = (2, 2, 1) + config.lattice.nk = nk + q_grid = bz.KGrid(nk, symmetries=[]) + rng = np.random.default_rng(11) + nvg = niv_pp + 2 + # asymmetric G_ij != G_ji obeying the physical conjugation symmetry G_ij(-nu) = conj(G_ji(nu)) + half = rng.standard_normal((nb, nb, nvg)) + 1j * rng.standard_normal((nb, nb, nvg)) + gloc = np.empty((nb, nb, 2 * nvg), dtype=np.complex64) + gloc[:, :, nvg:] = half + gloc[:, :, :nvg] = np.conj(np.swapaxes(half, 0, 1))[:, :, ::-1] + g_loc = GreensFunction(gloc) + g_mom = GreensFunction( + np.broadcast_to(gloc, (*nk, nb, nb, 2 * nvg)).copy(), has_compressed_q_dimension=False, nk=nk + ) + local_acbd = BubbleGenerator.create_generalized_chi0_pp_w0(g_loc, niv_pp, beta).permute_orbitals("abcd->acbd") + momentum = BubbleGenerator.create_generalized_chi0_q_pp_w0(g_mom, niv_pp, q_grid).decompress_q_dimension() + assert np.allclose(momentum.mat, local_acbd.mat[..., 0, :][None, None, None] / (-beta), atol=1e-4) diff --git a/tests/test_data/end_2_end/gchi_aux_q_dens_sum_rank_0.npy b/tests/test_data/end_2_end/chi_phys_q_dens_rank_0.npy similarity index 100% rename from tests/test_data/end_2_end/gchi_aux_q_dens_sum_rank_0.npy rename to tests/test_data/end_2_end/chi_phys_q_dens_rank_0.npy diff --git a/tests/test_data/end_2_end/gchi_aux_q_magn_sum_rank_0.npy b/tests/test_data/end_2_end/chi_phys_q_magn_rank_0.npy similarity index 100% rename from tests/test_data/end_2_end/gchi_aux_q_magn_sum_rank_0.npy rename to tests/test_data/end_2_end/chi_phys_q_magn_rank_0.npy diff --git a/tests/test_data/end_2_end/vrg_q_dens_right_rank_0.npy b/tests/test_data/end_2_end/vrg_q_dens_right_rank_0.npy new file mode 100644 index 0000000000000000000000000000000000000000..4c8b653e1bbc7fb44d302e21dc6db1234347de57 GIT binary patch literal 645248 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�u4C2CrkE2)=BB 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 Iyu#?rOpW5N36k%(_nxK#{h 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 Vs&xB!1sn`2xcD`v5O;Yj`+pVHOQQe) literal 0 HcmV?d00001 diff --git a/tests/test_eliashberg_end_to_end.py b/tests/test_eliashberg_end_to_end.py index c74273ce..756f5d5b 100644 --- a/tests/test_eliashberg_end_to_end.py +++ b/tests/test_eliashberg_end_to_end.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import os @@ -10,6 +10,7 @@ import numpy as np import pytest +import scipy as sp from mpi4py import MPI as RealMPI from dgamore import config, eliashberg_solver, dga_io @@ -63,16 +64,13 @@ def Waitall(reqs): yield folder, comm_mock -@pytest.mark.parametrize( - "niw_core, niv_core, niv_shell, save_fq, save_memory", - [(20, 20, 10, True, True), (20, 20, 10, False, True), (20, 20, 10, True, False), (20, 20, 10, False, False)], -) -def test_eliashberg_equation_without_local_part(setup, niw_core, niv_core, niv_shell, save_fq, save_memory): +@pytest.mark.parametrize("save_fq, save_memory", [(True, True), (False, True), (True, False), (False, False)]) +def test_eliashberg_equation_without_local_part(setup, save_fq, save_memory): folder, comm_mock = setup - config.box.niw_core = niw_core - config.box.niv_core = niv_core - config.box.niv_shell = niv_shell + config.box.niw_core = 20 + config.box.niv_core = 20 + config.box.niv_shell = 10 g_dmft, s_dmft, g2_dens, g2_magn = tuple(x[0] for x in dga_io.load_from_dmft_file_and_update_config()) @@ -82,7 +80,6 @@ def test_eliashberg_equation_without_local_part(setup, niw_core, niv_core, niv_s config.eliashberg.include_local_part = False config.eliashberg.save_fq = save_fq config.eliashberg.construct_fq_cheap = False - config.memory.save_memory_for_sde = save_memory config.memory.save_memory_for_fq = save_memory config.memory.save_memory_for_lanczos = save_memory @@ -94,20 +91,17 @@ def test_eliashberg_equation_without_local_part(setup, niw_core, niv_core, niv_s lambdas_sing, lambdas_trip, gaps_sing, gaps_trip = eliashberg_solver.solve( g_dga, g_dmft, u_loc, v_nonloc, comm_mock ) - assert np.allclose(lambdas_sing, np.array([16.00998764, 15.8037398, 14.97882938, 14.68343997]), atol=1e-4) - assert np.allclose(lambdas_trip, np.array([6.70800075, 6.70799438, 6.45388298, 6.45387878]), atol=1e-4) + assert np.allclose(lambdas_sing, np.array([16.00752, 15.802559, 14.981937, 14.684908]), atol=1e-2) + assert np.allclose(lambdas_trip, np.array([6.7059956, 6.705986, 6.456438, 6.4564347]), atol=1e-2) -@pytest.mark.parametrize( - "niw_core, niv_core, niv_shell, save_fq, save_memory", - [(20, 20, 10, True, True), (20, 20, 10, False, True), (20, 20, 10, True, False), (20, 20, 10, False, False)], -) -def test_eliashberg_equation_with_local_part(setup, niw_core, niv_core, niv_shell, save_fq, save_memory): +@pytest.mark.parametrize("save_fq, save_memory", [(True, True), (False, True), (True, False), (False, False)]) +def test_eliashberg_equation_with_local_part(setup, save_fq, save_memory): folder, comm_mock = setup - config.box.niw_core = niw_core - config.box.niv_core = niv_core - config.box.niv_shell = niv_shell + config.box.niw_core = 20 + config.box.niv_core = 20 + config.box.niv_shell = 10 g_dmft, s_dmft, g2_dens, g2_magn = tuple(x[0] for x in dga_io.load_from_dmft_file_and_update_config()) @@ -117,7 +111,6 @@ def test_eliashberg_equation_with_local_part(setup, niw_core, niv_core, niv_shel config.eliashberg.include_local_part = True config.eliashberg.save_fq = save_fq config.eliashberg.construct_fq_cheap = False - config.memory.save_memory_for_sde = save_memory config.memory.save_memory_for_fq = save_memory config.memory.save_memory_for_lanczos = save_memory @@ -129,5 +122,99 @@ def test_eliashberg_equation_with_local_part(setup, niw_core, niv_core, niv_shel lambdas_sing, lambdas_trip, gaps_sing, gaps_trip = eliashberg_solver.solve( g_dga, g_dmft, u_loc, v_nonloc, comm_mock ) - assert np.allclose(lambdas_sing, np.array([15.80373144, 14.68344285, 12.59292746, 10.81764214]), atol=1e-4) - assert np.allclose(lambdas_trip, np.array([6.70800083, 6.70799431, 6.45388305, 6.45387905]), atol=1e-4) + assert np.allclose(lambdas_sing, np.array([15.802544, 15.555848, 14.684908, 14.280717]), atol=1e-2) + assert np.allclose(lambdas_trip, np.array([6.705995, 6.7059927, 6.45644, 6.4564347]), atol=1e-2) + + +def _fft_index_map(nq: tuple, f) -> np.ndarray: + """Builds the [k, k'] index map on the flattened FFT grid from a per-axis index function f(i, j, n).""" + m = np.empty((int(np.prod(nq)), int(np.prod(nq))), dtype=int) + for a in range(nq[0]): + for b in range(nq[1]): + for c in range(nq[0]): + for d in range(nq[1]): + m[a * nq[1] + b, c * nq[1] + d] = (f(a, c, nq[0])) * nq[1] + (f(b, d, nq[1])) + return m + + +def test_kernel_matches_thesis_eliashberg_form_on_two_band_vertex(setup): + """Locks the full multi-orbital pairing kernel on the real two-band vertex against thesis Eq. (4.63): the + densified production matvec must equal the transparent index formula norm * sum_{ef} [sign + Gamma^{K-Q;vv'}_{e1f2} + Gamma^{(-K)-Q;v(-v')}_{e2f1}] G_{eh}(Q, v') conj(G_{gf}(Q, v')) Delta_{gh}(Q, v') + (pinning every layout, permute, FFT and the bubble-gap contraction for orbitally off-diagonal G), and its + leading eigenvalue spectrum must equal that of the independently densified thesis kernel + K^{vv'}_{1b2a}(K - Q) chi^{Q v'}_{0;acbd} Delta_{cd} with chi_{0;acbd} = G_{ad}(Q) G_{cb}(-Q) and the vertex + legs read as Gamma-thesis_{1234} = Gamma-stored_{2341}.""" + folder, comm_mock = setup + + config.box.niw_core = 20 + config.box.niv_core = 20 + config.box.niv_shell = 10 + + g_dmft, _, _, _ = tuple(x[0] for x in dga_io.load_from_dmft_file_and_update_config()) + + config.eliashberg.perform_eliashberg = True + config.output.output_path = folder + config.output.eliashberg_path = folder + config.eliashberg.include_local_part = True + config.eliashberg.save_fq = False + config.eliashberg.construct_fq_cheap = False + config.memory.save_memory_for_fq = False + config.memory.save_memory_for_lanczos = False + + u_loc = config.lattice.hamiltonian.get_local_u() + v_nonloc = config.lattice.hamiltonian.get_vq(config.lattice.q_grid) + g_dga = GreensFunction(np.load(f"{folder}/giwk_dga.npy"), nk=config.lattice.nk) + + captured = {} + real_solver = eliashberg_solver.solve_eliashberg_lanczos + + def capture_solver(gamma_r_pp, gchi0_q0_pp, ranks): + dense_holder = [] + + def fake_eigsh(op, k, tol, v0, which, maxiter): + n = op.shape[0] + dense_holder.append(np.column_stack([op.matvec(np.eye(n, dtype=np.complex64)[:, i]) for i in range(n)])) + lam, vec = np.linalg.eig(dense_holder[0]) + order = np.argsort(lam.real)[::-1][:k] + return lam.real[order], vec[:, order] + + channel = gamma_r_pp.channel.value + with patch("dgamore.eliashberg_solver.sp.sparse.linalg.eigsh", side_effect=fake_eigsh): + out = real_solver(gamma_r_pp, gchi0_q0_pp, ranks) + captured[channel] = {"gamma": gamma_r_pp.mat.copy(), "dense": dense_holder[0]} + return out + + with patch("dgamore.eliashberg_solver.solve_eliashberg_lanczos", side_effect=capture_solver): + eliashberg_solver.solve(g_dga, g_dmft, u_loc, v_nonloc, comm_mock) + + nq = config.lattice.q_grid.nk + nq_tot, o, beta = int(np.prod(nq)), config.sys.n_bands, config.sys.beta + niv_pp = min(config.box.niw_core // 2, config.box.niv_core // 2) + n2 = 2 * niv_pp + giwk = g_dga.cut_niv(niv_pp).compress_q_dimension().mat.astype(np.complex128) + kdiff = _fft_index_map(nq, lambda a, c, n: (a - c) % n) + kncross = _fft_index_map(nq, lambda a, c, n: (-a - c) % n) + norm = 0.5 / nq_tot / beta + + for ch, sign in (("sing", 1.0), ("trip", -1.0)): + # the solver fft's the passed object in place, so undo the transform to recover the momentum-space vertex + gam = sp.fft.ifftn(captured[ch]["gamma"], axes=(0, 1, 2)).astype(np.complex128) + gam = gam.reshape(nq_tot, o, o, o, o, n2, n2) + dense = captured[ch]["dense"].astype(np.complex128) + + gcm = np.transpose(gam, (0, 1, 4, 3, 2, 5, 6))[kncross][..., ::-1] + m_rec = norm * np.einsum( + "KQexfyvp,Qehp,Qgfp->KxyvQghp", sign * gam[kdiff] + gcm, giwk, np.conj(giwk), optimize=True + ) + assert np.allclose(dense, m_rec.reshape(dense.shape), atol=1e-5 * np.abs(dense).max()) + + gam_th = np.transpose(gam, (0, 2, 3, 4, 1, 5, 6)) + direct = np.einsum("KQxbyavp,Qadp,Qcbp->KxyvQcdp", gam_th[kdiff], giwk, np.conj(giwk), optimize=True) + crossed = np.einsum( + "KQxaybvp,Qadp,Qcbp->KxyvQcdp", gam_th[kncross][..., ::-1], giwk, np.conj(giwk), optimize=True + ) + m_th = (norm * sign * (direct + sign * crossed)).reshape(dense.shape) + ev_code = np.sort(np.linalg.eigvals(dense).real)[::-1][:10] + ev_thesis = np.sort(np.linalg.eigvals(m_th).real)[::-1][:10] + assert np.allclose(ev_code, ev_thesis, atol=1e-3) diff --git a/tests/test_eliashberg_solver.py b/tests/test_eliashberg_solver.py index 56c06b86..49732782 100644 --- a/tests/test_eliashberg_solver.py +++ b/tests/test_eliashberg_solver.py @@ -1,18 +1,32 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems +from unittest.mock import MagicMock, patch + import numpy as np import pytest +import dgamore.config as config +from dgamore import brillouin_zone as bz +from dgamore.bubble_gen import BubbleGenerator from dgamore.eliashberg_solver import ( _apply_gamma_pp, _apply_gchi0_pp, _chi0_to_matmul_layout, + create_local_gamma_ud_pp_w0, + create_local_gamma_ud_pp_w0_per_ineq, _gamma_to_matmul_layout, + solve_eliashberg_lanczos, + symmetrize_degenerate_gaps, + transform_vertex_loc_frequencies_w0, ) +from dgamore.four_point import FourPoint +from dgamore.greens_function import GreensFunction +from dgamore.local_four_point import LocalFourPoint +from dgamore.n_point_base import FrequencyNotation, SpinChannel def test_apply_gchi0_pp_matches_einsum(): @@ -26,7 +40,7 @@ def test_apply_gchi0_pp_matches_einsum(): ref = np.einsum("xyzabcdv,xyzcdv->xyzabv", chi0, gap, optimize=True) got = _apply_gchi0_pp(_chi0_to_matmul_layout(chi0), gap.ravel(), o) assert got.shape == ref.shape - assert np.allclose(got, ref, rtol=1e-10) + assert np.allclose(got, ref, atol=1e-10) @pytest.mark.parametrize("nv", [6, 3]) @@ -40,4 +54,462 @@ def test_apply_gamma_pp_matches_einsum(nv): ref = np.einsum("xyzacbdvp,xyzcdp->xyzabv", gamma, gap_gg, optimize=True) got = _apply_gamma_pp(_gamma_to_matmul_layout(gamma), gap_gg, o) assert got.shape == ref.shape - assert np.allclose(got, ref, rtol=1e-10) + assert np.allclose(got, ref, atol=1e-10) + + +def _make_pp_chi_and_bubble( + o: int, niv_pp: int, beta: float, seed: int +) -> tuple[LocalFourPoint, LocalFourPoint, GreensFunction]: + """Builds a random crossing-symmetric pp susceptibility (J chi J = chi) and the matching diagonal bare pp bubble + from a random symmetric local Green's function (G_12(v) = G_21(v), no SOC).""" + rng = np.random.default_rng(seed) + g_mat = rng.standard_normal((o, o, 2 * (niv_pp + 2))) + 1j * rng.standard_normal((o, o, 2 * (niv_pp + 2))) + g_mat = 0.5 * (g_mat + g_mat.transpose(1, 0, 2)) + 2.0 * np.eye(o)[:, :, None] + g = GreensFunction(g_mat) + chi0 = BubbleGenerator.create_generalized_chi0_pp_w0(g, niv_pp, beta).extend_vn_to_diagonal() + shape = (o, o, o, o, 1, 2 * niv_pp, 2 * niv_pp) + chi_mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + chi_mat = 0.5 * (chi_mat + _j_conjugate(chi_mat)) + # compound-identity shift: J-symmetric, keeps every inversion well-conditioned for any band count + ident = np.einsum("ad,bc,vp->abcdvp", np.eye(o), np.eye(o), np.eye(2 * niv_pp)) + chi_mat += 4.0 * o * ident[:, :, :, :, None] + chi = LocalFourPoint(chi_mat, SpinChannel.UD, 1, 2, True, True, FrequencyNotation.PP) + return chi, chi0, g + + +def _j_conjugate(mat: np.ndarray) -> np.ndarray: + """Conjugates a full-index pp tensor with the crossing operator J (swap both orbital pairs, flip both fermionic + frequencies).""" + return np.einsum("abcdwvp->cdabwvp", mat)[..., ::-1, ::-1] + + +def test_local_gamma_ud_pp_w0_matches_b26_for_single_band(): + """For one band and crossing-symmetric chi the J-decorated BSE inversion is identical to the old flipped-bubble + B.26 form, locking backwards compatibility of the single-orbital results.""" + beta, niv_pp = 12.5, 4 + chi, chi0, _ = _make_pp_chi_and_bubble(1, niv_pp, beta, seed=7) + chi0_flipped = chi0.flip_frequency_axis(-1) + gamma_old = ((chi - chi0_flipped).invert() + chi0_flipped.invert()).scale(beta**2) + gamma_new = create_local_gamma_ud_pp_w0(chi, chi0, beta) + assert gamma_new.mat.shape == gamma_old.mat.shape + assert np.allclose(gamma_new.mat, gamma_old.mat, atol=1e-3) + + +@pytest.mark.parametrize("o", [2, 3, 4, 5]) +def test_local_gamma_ud_pp_w0_multiorbital_preserves_crossing_symmetry(o): + """For more than one band the J-decorated inversion keeps Gamma crossing-symmetric (J Gamma J = Gamma) and + deviates from the old flipped-bubble form, whose bubble misses the orbital pair permutation of the crossing + operator.""" + beta, niv_pp = 10.0, 3 + chi, chi0, _ = _make_pp_chi_and_bubble(o, niv_pp, beta, seed=8) + gamma_new = create_local_gamma_ud_pp_w0(chi, chi0, beta) + assert gamma_new.mat.shape == (o, o, o, o, 1, 2 * niv_pp, 2 * niv_pp) + assert np.allclose(gamma_new.mat, _j_conjugate(gamma_new.mat), atol=1e-3) + chi0_flipped = chi0.flip_frequency_axis(-1) + gamma_old = ((chi - chi0_flipped).invert() + chi0_flipped.invert()).scale(beta**2) + assert not np.allclose(gamma_old.mat, gamma_new.mat, atol=1e-3) + + +@pytest.mark.parametrize("o", [2, 3, 4, 5]) +def test_local_gamma_ud_pp_w0_satisfies_pp_bse(o): + """Locks every orbital and frequency index of create_local_gamma_ud_pp_w0: the helper matches a float64 compound + reference of Gamma^{vv'}_1234 = beta^2 [chi0 J - chi0 chi^{-1} chi0]^{-1;vv'}_1234, the reference F (leg-amputated + chi) rebuilds chi through the raw leg einsum, and (Gamma, F) satisfy the crossing-decoupled pp BSE written with + explicit indices (free orbital indices 1234, summed indices alphabetical from the left-most object).""" + beta, niv_pp = 10.0, 3 + chi_obj, chi0_obj, g = _make_pp_chi_and_bubble(o, niv_pp, beta, seed=11) + n2 = 2 * niv_pp + dim = o * o * n2 + + def to_c(x): + return np.transpose(x, (0, 2, 4, 3, 1, 5)).reshape(dim, dim) + + def to_t(m): + return np.transpose(m.reshape(o, o, n2, o, o, n2), (0, 4, 1, 3, 2, 5)) + + chi = chi_obj.mat[:, :, :, :, 0].astype(np.complex128) + chi0 = chi0_obj.mat[:, :, :, :, 0].astype(np.complex128) + gv = g.mat[:, :, 2:-2].astype(np.complex128) + gm = gv[:, :, ::-1] + chi0_j = np.einsum("abcdvp->adcbvp", chi0)[..., ::-1] + assert np.allclose(chi0_j, -beta * np.einsum("abv,cdv,vp->abcdvp", gv, gm, np.eye(n2)[:, ::-1]), atol=1e-4) + gamma_ref = to_t(beta**2 * np.linalg.inv(to_c(chi0_j) - to_c(chi0) @ np.linalg.inv(to_c(chi)) @ to_c(chi0))) + gamma = create_local_gamma_ud_pp_w0(chi_obj, chi0_obj, beta) + assert np.allclose(gamma.mat[:, :, :, :, 0], gamma_ref, atol=1e-3) + f = to_t(-(beta**2) * np.linalg.inv(to_c(chi0)) @ to_c(chi) @ np.linalg.inv(to_c(chi0))) + chi_back = -np.einsum("xav,ycv,abcdvp,dup,bzp->xzyuvp", gv, gm, f, gv, gm, optimize=True) + assert np.allclose(chi_back, chi, atol=1e-4) + # BSE: F^{vv'}_1234 = Gamma^{vv'}_1234 - (1/beta) sum_{v1,abcd} Gamma^{v v1}_{1a3b} G_bc(v1) G_ad(-v1) F^{(-v1)v'}_{d2c4} + ladder = np.einsum("xfyevw,ehw,fgw,gzhuwp->xzyuvp", gamma_ref, gv, gm, f[..., ::-1, :], optimize=True) + assert np.allclose(f, gamma_ref - ladder / beta, atol=1e-3) + + +@pytest.mark.parametrize("o", [5, 4, 3, 2, 1]) +def test_local_gamma_ud_pp_w0_satisfies_decoupled_singlet_triplet_bse(o): + """Gamma from create_local_gamma_ud_pp_w0 fulfills the decoupled singlet/triplet pp BSEs of thesis Eqs. + (3.51)/(3.52) on the J-even/odd blocks, F_s/t = Gamma_s/t +/- 1/(2 beta^2) Gamma_s/t chi0 F_s/t, in compound + pp space (rows {1,3,v}, cols {4,2,v'}); J is the crossing operator, chi0*J its exact matrix realization.""" + beta, niv_pp = 10.0, 3 + chi_obj, chi0_obj, _ = _make_pp_chi_and_bubble(o, niv_pp, beta, seed=23) + n2 = 2 * niv_pp + dim = o * o * n2 + + def to_c(x): + return np.transpose(x, (0, 2, 4, 3, 1, 5)).reshape(dim, dim) + + chi = to_c(chi_obj.mat[:, :, :, :, 0].astype(np.complex128)) + chi0 = to_c(chi0_obj.mat[:, :, :, :, 0].astype(np.complex128)) + chi0_j = to_c(np.einsum("abcdvp->adcbvp", chi0_obj.mat[:, :, :, :, 0].astype(np.complex128))[..., ::-1]) + j = to_c(np.einsum("ab,cd,vp->abcdvp", np.eye(o), np.eye(o), np.eye(n2)[:, ::-1])) + # the crossing operator squares to one, realizes chi0*J and commutes with the (crossing-symmetric) inputs + assert np.allclose(j @ j, np.eye(dim), atol=1e-12) + assert np.allclose(chi0 @ j, chi0_j, atol=1e-12) + assert np.allclose(j @ chi0, chi0 @ j, atol=1e-12) + assert np.allclose(j @ chi, chi @ j, atol=1e-4) + + gamma_ref = beta**2 * np.linalg.inv(chi0_j - chi0 @ np.linalg.inv(chi) @ chi0) + gamma_code = to_c(create_local_gamma_ud_pp_w0(chi_obj, chi0_obj, beta).mat[:, :, :, :, 0].astype(np.complex128)) + assert np.allclose(gamma_code, gamma_ref, atol=1e-3) + + f = -(beta**2) * np.linalg.inv(chi0) @ chi @ np.linalg.inv(chi0) + p_plus = 0.5 * (np.eye(dim) + j) + p_minus = 0.5 * (np.eye(dim) - j) + f_s, gamma_s = 2 * f @ p_plus, 2 * gamma_ref @ p_plus + f_t, gamma_t = 2 * f @ p_minus, 2 * gamma_ref @ p_minus + assert np.allclose(f_s, gamma_s + gamma_s @ chi0 @ f_s / (2 * beta**2), atol=1e-3) + assert np.allclose(f_t, gamma_t - gamma_t @ chi0 @ f_t / (2 * beta**2), atol=1e-3) + + +@pytest.mark.parametrize("o", [5, 4, 3, 2, 1]) +def test_transform_vertex_loc_frequencies_w0_is_crossed_slot_form(o): + """transform_vertex_loc_frequencies_w0 returns -F^{(v-v')v(-v')}_{1432} (crossed slot of thesis Eq. 4.49); the + orbital permutation to 1432 only shows up for more than one band.""" + rng = np.random.default_rng(6) + niw, niv, niv_pp = 6, 5, 3 + config.box.niw_core = niw + shape = (o, o, o, o, 2 * niw + 1, 2 * niv, 2 * niv) + mat = (rng.standard_normal(shape) + 1j * rng.standard_normal(shape)).astype(np.complex64) + f_loc = LocalFourPoint(mat.copy(), SpinChannel.UD, 1, 2, True, True) + got = transform_vertex_loc_frequencies_w0(f_loc, niv_pp) + vn = np.arange(-niv_pp, niv_pp) + expected = np.zeros((o, o, o, o, 2 * niv_pp, 2 * niv_pp), dtype=mat.dtype) + for i, n in enumerate(vn): + for j, m in enumerate(vn): + # crossed slot: orbitals 1432 ('abcd->adcb'), bosonic index v-v', second fermionic frequency flipped + expected[..., i, j] = -mat[:, :, :, :, niw + n - m, niv + n, niv - m - 1].transpose(0, 3, 2, 1) + assert got.mat.shape == expected.shape + assert got.frequency_notation == FrequencyNotation.PP + assert np.allclose(got.mat, expected, atol=1e-6) + + +@pytest.mark.parametrize("o", [2, 1]) +def test_pairing_vertex_contraction_uses_triqs_leg_order(o): + """The 'abcd->badc' permute makes the vertex contraction implement Delta_ab = sum_cd Gamma_cadb X_cd (TRIQS leg + order); the raw w2dynamics-order layout only agrees for a single band.""" + rng = np.random.default_rng(3) + nq, nv = (2, 2, 1), 4 + shape = (*nq, o, o, o, o, nv, nv) + gamma = (rng.standard_normal(shape) + 1j * rng.standard_normal(shape)).astype(np.complex64) + x = (rng.standard_normal((*nq, o, o, nv)) + 1j * rng.standard_normal((*nq, o, o, nv))).astype(np.complex64) + ref = np.einsum("xyzcadbvp,xyzcdp->xyzabv", gamma, x, optimize=True) + badc = FourPoint( + gamma.copy(), SpinChannel.SING, nq, 0, 2, True, True, False, FrequencyNotation.PP + ).permute_orbitals("abcd->badc") + fixed = _apply_gamma_pp(_gamma_to_matmul_layout(badc.mat), x, o) + raw = _apply_gamma_pp(_gamma_to_matmul_layout(gamma), x, o) + assert np.allclose(fixed, ref, atol=1e-4) + assert (o == 1) == np.allclose(raw, ref, atol=1e-4) + + +def _graded_orbital_symmetrization(gamma: np.ndarray, generators: list[tuple[tuple[int, ...], int]]) -> np.ndarray: + """Averages a rank-4 orbital tensor over the group generated by the given (permutation, character) pairs. With all + characters +1 this is a plain symmetrization; a character -1 makes the vertex odd under that permutation.""" + group = {(0, 1, 2, 3): 1.0} + changed = True + while changed: + changed = False + for p, s in list(group.items()): + for q, sq in generators: + composition = tuple(p[q[i]] for i in range(4)) + if composition not in group: + group[composition] = s * sq + changed = True + return sum(s * np.transpose(gamma, p) for p, s in group.items()) / len(group) + + +def _matvec_direct_term_single_slice( + gamma: np.ndarray, g_plus: np.ndarray, g_minus: np.ndarray, gap: np.ndarray, n_bands: int +) -> np.ndarray: + """Applies the direct term of the Eliashberg matvec for a single momentum/frequency slice, exactly as in + :func:`solve_eliashberg_lanczos`: bare pp bubble times gap (see :func:`_apply_gchi0_pp`), then the + ``"abcd->badc"``-permuted pairing vertex contraction (see :func:`_apply_gamma_pp`).""" + chi0 = np.einsum("ad,bc->abcd", g_plus, g_minus)[None, None, None, ..., None] + gap_gg = _apply_gchi0_pp(_chi0_to_matmul_layout(chi0), gap[None, None, None, ..., None].ravel(), n_bands) + vertex = FourPoint( + gamma[None, None, None, ..., None, None].copy(), + SpinChannel.SING, + (1, 1, 1), + 0, + 2, + True, + True, + False, + FrequencyNotation.PP, + ).permute_orbitals("abcd->badc") + return _apply_gamma_pp(_gamma_to_matmul_layout(vertex.mat), gap_gg, n_bands)[0, 0, 0, :, :, 0] + + +@pytest.mark.parametrize("channel", [SpinChannel.SING, SpinChannel.TRIP]) +def test_matvec_direct_term_matches_thesis_eq_4_40_on_physical_sector(channel): + """On the physical symmetry class -- pairing vertex with its particle-swap (1432, 3214) and static time-reversal + (dcba) symmetries, G(k) symmetric (time reversal plus inversion) and the gap in the SPOT sector (Delta = +/- + Delta^T for singlet/triplet) -- the matvec direct term is identical to Eq. (4.40) of the thesis and preserves the + sector. For a generic (unsymmetrized) vertex the two wirings differ, so the equivalence is a property of the + symmetry class, not of the contraction pattern.""" + rng = np.random.default_rng(4) + o = 3 + sign = 1 if channel == SpinChannel.SING else -1 + + gamma = (rng.standard_normal((o,) * 4) + 1j * rng.standard_normal((o,) * 4)).astype(np.complex64) + gamma_sym = _graded_orbital_symmetrization(gamma, [((0, 3, 2, 1), sign), ((2, 1, 0, 3), sign), ((3, 2, 1, 0), 1)]) + + g_k = rng.standard_normal((o, o)) + 1j * rng.standard_normal((o, o)) + g_k = (0.5 * (g_k + g_k.T)).astype(np.complex64) # TR + inversion: G(k) = G(k)^T = G(-k) + + for _ in range(4): + gap = rng.standard_normal((o, o)) + 1j * rng.standard_normal((o, o)) + gap = (0.5 * (gap + sign * gap.T)).astype(np.complex64) # SPOT-allowed static sector + + got = _matvec_direct_term_single_slice(gamma_sym, g_k, g_k, gap, o) + ref = np.einsum("xbya,ad,cb,dc->xy", gamma_sym, g_k, g_k, gap, optimize=True) + assert np.allclose(got, ref, atol=1e-4) + assert np.allclose(got, sign * got.T, atol=1e-4) + + got_generic = _matvec_direct_term_single_slice(gamma, g_k, g_k, gap, o) + ref_generic = np.einsum("xbya,ad,cb,dc->xy", gamma, g_k, g_k, gap, optimize=True) + assert not np.allclose(got_generic, ref_generic, atol=1e-4) + + +@pytest.mark.parametrize("o", [2, 1]) +def test_badc_permute_is_noop_for_swap_and_tr_symmetric_pairing_vertex(o): + """'abcd->badc' composes from the physical vertex symmetries (badc = dcba composed with cdab), so for a pairing + vertex carrying the particle-swap (1432, 3214) and static time-reversal (dcba) symmetries the w2dynamics and TRIQS + leg orders give identical contractions for any gap; for a generic tensor they differ (except for a single band).""" + rng = np.random.default_rng(5) + nq, nv = (1, 1, 1), 1 + + gamma = (rng.standard_normal((o,) * 4) + 1j * rng.standard_normal((o,) * 4)).astype(np.complex64) + gamma_sym = _graded_orbital_symmetrization(gamma, [((0, 3, 2, 1), 1), ((2, 1, 0, 3), 1), ((3, 2, 1, 0), 1)]) + x = (rng.standard_normal((*nq, o, o, nv)) + 1j * rng.standard_normal((*nq, o, o, nv))).astype(np.complex64) + + def contract(mat): + padded = mat[None, None, None, ..., None, None].copy() + return _apply_gamma_pp(_gamma_to_matmul_layout(padded), x, o) + + def badc(mat): + return ( + FourPoint( + mat[None, None, None, ..., None, None].copy(), + SpinChannel.SING, + nq, + 0, + 2, + True, + True, + False, + FrequencyNotation.PP, + ) + .permute_orbitals("abcd->badc") + .mat[0, 0, 0, :, :, :, :, 0, 0] + ) + + assert np.allclose(contract(badc(gamma_sym)), contract(gamma_sym), atol=1e-4) + assert (o == 1) == np.allclose(contract(badc(gamma)), contract(gamma), atol=1e-4) + + +@pytest.mark.parametrize("o", [2, 3, 4, 5]) +@pytest.mark.parametrize("channel", [SpinChannel.SING, SpinChannel.TRIP]) +def test_degenerate_decoupled_bands_reproduce_single_band_kernel(channel, o): + """o decoupled, degenerate bands (same-band-only pairing vertex, orbital-diagonal Green's function) must give + a pairing kernel that is o identical copies of the single-band kernel with vanishing inter-band blocks, so the + eigenvalue spectrum is the single-band one, o-fold degenerate; run through the production solve_eliashberg_lanczos + matvec (eigsh is intercepted and densified).""" + nq, niv_pp, beta = (2, 2, 1), 2, 10.0 + nq_tot, n2 = int(np.prod(nq)), 2 * niv_pp + config.lattice.nk = nq + config.lattice.nq = nq + config.lattice.q_grid = bz.KGrid(nq, symmetries=[]) + config.lattice.k_grid = config.lattice.q_grid + config.sys.beta = beta + config.eliashberg.n_eig = 2 + config.eliashberg.epsilon = 1e-10 + config.eliashberg.symmetry = "random" + config.logger = MagicMock() + + rng = np.random.default_rng(21) + shape_g1 = (nq_tot, 1, 1, 1, 1, n2, n2) + gamma1 = rng.standard_normal(shape_g1) + 1j * rng.standard_normal(shape_g1) + chi1 = rng.standard_normal(shape_g1[:-1]) + 1j * rng.standard_normal(shape_g1[:-1]) + + gamma2 = np.zeros((nq_tot, o, o, o, o, n2, n2), dtype=complex) + chi2 = np.zeros((nq_tot, o, o, o, o, n2), dtype=complex) + for band in range(o): + gamma2[:, band, band, band, band] = gamma1[:, 0, 0, 0, 0] + for a in range(o): + for c in range(o): + chi2[:, a, c, c, a] = chi1[:, 0, 0, 0, 0] + + captured = [] + + def fake_eigsh(op, k, tol, v0, which, maxiter): + n = op.shape[0] + dense = np.column_stack([op.matvec(np.eye(n, dtype=np.complex64)[:, i]) for i in range(n)]) + captured.append(dense) + lam, vec = np.linalg.eig(dense) + order = np.argsort(lam.real)[::-1][:k] + return lam.real[order], vec[:, order] + + with patch("dgamore.eliashberg_solver.sp.sparse.linalg.eigsh", side_effect=fake_eigsh): + solve_eliashberg_lanczos( + FourPoint(gamma1.copy(), channel, nq, 0, 2, True, True, True, FrequencyNotation.PP), + FourPoint(chi1.copy(), SpinChannel.NONE, nq, 0, 1, True, True, True, FrequencyNotation.PP), + (0, 0), + ) + solve_eliashberg_lanczos( + FourPoint(gamma2, channel, nq, 0, 2, True, True, True, FrequencyNotation.PP), + FourPoint(chi2, SpinChannel.NONE, nq, 0, 1, True, True, True, FrequencyNotation.PP), + (0, 0), + ) + m1, m2 = captured + + # gap flattening [k, a, b, v]: build the (a, b) sector masks of the multi-band gap space + idx = np.arange(nq_tot * o * o * n2) + a_idx = (idx // (o * n2)) % o + b_idx = idx // n2 % o + expected = np.zeros_like(m2) + for band in range(o): + sector = np.flatnonzero((a_idx == band) & (b_idx == band)) + expected[np.ix_(sector, sector)] = m1 + assert np.allclose(m2, expected, atol=1e-4) + + ev1 = np.linalg.eigvals(m1) + ev2 = np.linalg.eigvals(m2) + expected_ev = np.concatenate([np.tile(ev1, o), np.zeros(len(ev2) - o * len(ev1))]) + assert np.allclose(np.sort_complex(ev2), np.sort_complex(expected_ev), atol=1e-3) + + +def _make_p_wave_doublet(nk: int = 6, n2: int = 4) -> tuple[np.ndarray, np.ndarray, tuple]: + """Builds orthonormal p_x/p_y-like gap columns sin(k_x) g(v) and sin(k_y) g(v) on a small single-band grid.""" + gap_shape = (nk, nk, 1, 1, 1, n2) + k = 2 * np.pi * np.arange(nk) / nk + g_v = np.linspace(1.0, 0.5, n2) + px = (np.sin(k)[:, None, None, None, None, None] * g_v * np.ones(gap_shape)).ravel() + py = (np.sin(k)[None, :, None, None, None, None] * g_v * np.ones(gap_shape)).ravel() + px = px / np.linalg.norm(px) + py = py / np.linalg.norm(py) + return px.astype(np.complex128), py, gap_shape + + +def _mirror_y_column(column: np.ndarray, gap_shape: tuple) -> np.ndarray: + """Applies the mirror k_y -> -k_y to a flattened gap column.""" + idx = (gap_shape[1] - np.arange(gap_shape[1])) % gap_shape[1] + return column.reshape(gap_shape)[:, idx].ravel() + + +def test_symmetrize_degenerate_gaps_recovers_mirror_partners(): + """An obliquely mixed, complex-phased degenerate doublet is orthonormalized and rotated back to the + mirror-adapted p_x-like (even) and p_y-like (odd) partners with deterministic phases; the vector of the + non-degenerate eigenvalue is only phase-fixed.""" + px, py, gap_shape = _make_p_wave_doublet() + rng = np.random.default_rng(24) + extra = rng.standard_normal(px.size) + 1j * rng.standard_normal(px.size) + extra /= np.linalg.norm(extra) + mix = np.array([[1.0, 0.6 + 0.3j], [0.2 - 0.4j, 0.8]]) + doublet = np.stack([px, py], axis=1) @ mix + gaps = np.concatenate([doublet, extra[:, None]], axis=1) + fixed = symmetrize_degenerate_gaps(np.array([0.5, 0.5, 0.2]), gaps, gap_shape) + overlap = fixed[:, :2].conj().T @ fixed[:, :2] + assert np.allclose(overlap, np.eye(2), atol=1e-12) + assert np.allclose(_mirror_y_column(fixed[:, 0], gap_shape), fixed[:, 0], atol=1e-12) + assert np.allclose(_mirror_y_column(fixed[:, 1], gap_shape), -fixed[:, 1], atol=1e-12) + assert abs(np.vdot(fixed[:, 0], px)) > 1 - 1e-12 + assert abs(np.vdot(fixed[:, 1], py)) > 1 - 1e-12 + assert abs(np.vdot(fixed[:, 2], extra)) > 1 - 1e-12 + + +def test_symmetrize_degenerate_gaps_is_idempotent(): + """Applying the symmetrization twice gives the same result as applying it once (fixed point).""" + px, py, gap_shape = _make_p_wave_doublet() + mix = np.array([[1.0, 0.5 - 0.2j], [0.3 + 0.1j, 1.0]]) + gaps = np.stack([px, py], axis=1) @ mix + once = symmetrize_degenerate_gaps(np.array([0.7, 0.7]), gaps, gap_shape) + twice = symmetrize_degenerate_gaps(np.array([0.7, 0.7]), once, gap_shape) + assert np.allclose(once, twice, atol=1e-12) + + +def test_symmetrize_degenerate_gaps_skips_dependent_vectors(): + """A cluster of (numerically) linearly dependent vectors is left untouched instead of amplifying noise.""" + px, _, gap_shape = _make_p_wave_doublet() + gaps = np.stack([px, px * (1 + 1e-15)], axis=1) + fixed = symmetrize_degenerate_gaps(np.array([0.5, 0.5]), gaps, gap_shape) + assert np.allclose(fixed, gaps, atol=1e-12) + + +def _assemble_two_atom_system(niv_pp: int, beta: float) -> tuple: + """Builds a two-atom (1 + 2 bands) block-structured full chi and Green's function from independent per-atom + blocks, plus the per-atom inputs for reference.""" + chi_a, _, g_a = _make_pp_chi_and_bubble(1, niv_pp, beta, seed=31) + chi_b, _, g_b = _make_pp_chi_and_bubble(2, niv_pp, beta, seed=32) + n2, nvg = 2 * niv_pp, g_a.mat.shape[-1] + chi_full = np.zeros((3, 3, 3, 3, 1, n2, n2), dtype=complex) + chi_full[:1, :1, :1, :1] = chi_a.mat + chi_full[1:, 1:, 1:, 1:] = chi_b.mat + g_full = np.zeros((3, 3, nvg), dtype=complex) + g_full[:1, :1] = g_a.mat + g_full[1:, 1:] = g_b.mat + chi_full_obj = LocalFourPoint(chi_full, SpinChannel.UD, 1, 2, True, True, FrequencyNotation.PP) + return chi_full_obj, GreensFunction(g_full), (chi_a, g_a), (chi_b, g_b) + + +def test_local_gamma_ud_pp_w0_per_ineq_assembles_block_structure(): + """For two inequivalent atoms the full multi-band chi is block-structured and its compound pp matrix singular + (the La3Ni2O7 crash); the per-ineq driver inverts each atom's block separately, reproduces the direct per-block + result, and leaves all cross-atom components zero.""" + beta, niv_pp = 10.0, 3 + config.dmft.n_ineq = 2 + config.dmft.ineq_ordering = [1, 2] + config.dmft.n_bands_per_ineq = [1, 2] + chi_full_obj, g_full, (chi_a, g_a), (chi_b, g_b) = _assemble_two_atom_system(niv_pp, beta) + chi0_full = BubbleGenerator.create_generalized_chi0_pp_w0(g_full, niv_pp, beta).extend_vn_to_diagonal() + with pytest.raises(np.linalg.LinAlgError): + create_local_gamma_ud_pp_w0(chi_full_obj, chi0_full, beta) + gamma_full = create_local_gamma_ud_pp_w0_per_ineq(chi_full_obj, g_full, beta) + chi0_a = BubbleGenerator.create_generalized_chi0_pp_w0(g_a, niv_pp, beta).extend_vn_to_diagonal() + chi0_b = BubbleGenerator.create_generalized_chi0_pp_w0(g_b, niv_pp, beta).extend_vn_to_diagonal() + assert np.allclose(gamma_full.mat[:1, :1, :1, :1], create_local_gamma_ud_pp_w0(chi_a, chi0_a, beta).mat, atol=1e-6) + assert np.allclose(gamma_full.mat[1:, 1:, 1:, 1:], create_local_gamma_ud_pp_w0(chi_b, chi0_b, beta).mat, atol=1e-6) + cross = gamma_full.mat.copy() + cross[:1, :1, :1, :1] = 0.0 + cross[1:, 1:, 1:, 1:] = 0.0 + assert np.allclose(cross, 0.0, atol=1e-12) + + +def test_local_gamma_ud_pp_w0_per_ineq_reuses_repeated_atoms(): + """Repeated entries in ineq_ordering (the same inequivalent atom at several positions) are computed once and + written identically into every position.""" + beta, niv_pp = 10.0, 3 + config.dmft.n_ineq = 1 + config.dmft.ineq_ordering = [1, 1] + config.dmft.n_bands_per_ineq = [2] + chi_a, _, g_a = _make_pp_chi_and_bubble(2, niv_pp, beta, seed=33) + n2, nvg = 2 * niv_pp, g_a.mat.shape[-1] + chi_full = np.zeros((4, 4, 4, 4, 1, n2, n2), dtype=complex) + g_full = np.zeros((4, 4, nvg), dtype=complex) + for sl in (slice(0, 2), slice(2, 4)): + chi_full[sl, sl, sl, sl] = chi_a.mat + g_full[sl, sl] = g_a.mat + chi_full_obj = LocalFourPoint(chi_full, SpinChannel.UD, 1, 2, True, True, FrequencyNotation.PP) + gamma_full = create_local_gamma_ud_pp_w0_per_ineq(chi_full_obj, GreensFunction(g_full), beta) + assert np.allclose(gamma_full.mat[:2, :2, :2, :2], gamma_full.mat[2:, 2:, 2:, 2:], atol=1e-12) + chi0_a = BubbleGenerator.create_generalized_chi0_pp_w0(g_a, niv_pp, beta).extend_vn_to_diagonal() + assert np.allclose(gamma_full.mat[:2, :2, :2, :2], create_local_gamma_ud_pp_w0(chi_a, chi0_a, beta).mat, atol=1e-6) diff --git a/tests/test_four_point.py b/tests/test_four_point.py index d33325a1..0b5f0b84 100644 --- a/tests/test_four_point.py +++ b/tests/test_four_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems from copy import deepcopy @@ -126,6 +126,116 @@ def test_mul_with_scalar_and_array(small_fourpoint): assert np.allclose(res2.mat, fp.mat * 2.0) +def test_scale_in_place_multiplies_and_returns_self(small_fourpoint): + """scale(factor) multiplies mat in place (copy=False default), returns self, and stays complex64.""" + fp = small_fourpoint + before = fp.mat.copy() + out = fp.scale(3.0) + assert out is fp + assert np.array_equal(fp.mat, (before * 3.0).astype(np.complex64)) + assert fp.mat.dtype == np.complex64 + + +def test_scale_copy_true_leaves_original_untouched(small_fourpoint): + """scale(factor, copy=True) returns a new scaled object and leaves self unchanged.""" + fp = small_fourpoint + before = fp.mat.copy() + out = fp.scale(-2.0, copy=True) + assert out is not fp + assert np.array_equal(out.mat, (before * -2.0).astype(np.complex64)) + assert np.array_equal(fp.mat, before) + + +def test_scale_rejects_non_scalar(small_fourpoint): + """scale only accepts numbers (mirrors __mul__).""" + with pytest.raises(ValueError): + small_fourpoint.scale(np.ones(3)) + + +def test_copy_returns_independent_deep_copy(small_fourpoint): + """copy() returns a new, independent deep copy whose mutation does not affect the original.""" + fp = small_fourpoint + c = fp.copy() + assert c is not fp + assert c.mat is not fp.mat + assert np.array_equal(c.mat, fp.mat) + before = fp.mat.copy() + c.mat[...] = 0.0 + assert np.array_equal(fp.mat, before) + + +def _kernel_block(rng, channel, num_vn=1): + """Builds a half-niw, compressed-q FourPoint matching the self-energy-kernel layout (num_vn 1 or 2).""" + nq = (4, 4, 1) + qtot, o, niw, niv = 16, 2, 3, 3 + shape = (qtot, o, o, o, o, niw + 1) + (2 * niv,) * num_vn + mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + return FourPoint(mat, channel, nq, 1, num_vn, False, True, True, FrequencyNotation.PH) + + +def test_add_inplace_equals_out_of_place_and_returns_self(rng): + """add(other, copy=False) accumulates into self in place, returns self, bit-equal to self + other.""" + a, b = _kernel_block(rng, SpinChannel.NONE), _kernel_block(rng, SpinChannel.DENS) + ref = deepcopy(a) + deepcopy(b) + out = a.add(b, copy=False) + assert out is a + assert np.array_equal(a.mat, ref.mat) + assert a.channel == ref.channel == SpinChannel.DENS + + +def test_sub_inplace_equals_out_of_place_and_returns_self(rng): + """sub(other, copy=False) subtracts into self in place, returns self, bit-equal to self - other.""" + a, b = _kernel_block(rng, SpinChannel.DENS), _kernel_block(rng, SpinChannel.MAGN) + ref = deepcopy(a) - deepcopy(b) + out = a.sub(b, copy=False) + assert out is a + assert np.array_equal(a.mat, ref.mat) + + +def test_add_inplace_with_scaled_other_matches_reference(rng): + """The kernel-accumulation idiom k.add(other.scale(3), copy=False) equals k + 3 * other.""" + a, b = _kernel_block(rng, SpinChannel.NONE), _kernel_block(rng, SpinChannel.MAGN) + ref = deepcopy(a) + 3 * deepcopy(b) + a.add(b.scale(3.0), copy=False) + assert np.array_equal(a.mat, ref.mat) + + +def test_add_copy_true_is_nondestructive(rng): + """add(other) (copy=True default) returns a new object and leaves self unchanged (unchanged behavior).""" + a, b = _kernel_block(rng, SpinChannel.DENS), _kernel_block(rng, SpinChannel.DENS) + before = a.mat.copy() + out = a.add(b) + assert out is not a + assert np.array_equal(a.mat, before) + + +def test_add_inplace_rejects_non_fourpoint(rng): + """copy=False is only defined between two FourPoints; a scalar/array/interaction operand raises.""" + a = _kernel_block(rng, SpinChannel.DENS) + with pytest.raises(NotImplementedError): + a.add(2.0, copy=False) + + +def test_add_inplace_rejects_vn_extension(rng): + """copy=False refuses to diagonally extend self (num_vn=1 += num_vn=2), which would allocate a larger array.""" + a, b = _kernel_block(rng, SpinChannel.NONE, num_vn=1), _kernel_block(rng, SpinChannel.DENS, num_vn=2) + with pytest.raises(ValueError): + a.add(b, copy=False) + + +def test_add_inplace_reverts_other_niw_range(rng): + """copy=False converts a full-niw other to half for the op, then restores its range (non-destructive to other).""" + nq, qtot, o, niw, niv = (4, 4, 1), 16, 2, 3, 3 + a = _kernel_block(rng, SpinChannel.DENS) # half niw range + full_shape = (qtot, o, o, o, o, 2 * niw + 1, 2 * niv) + full_mat = rng.standard_normal(full_shape) + 1j * rng.standard_normal(full_shape) + b = FourPoint(full_mat, SpinChannel.MAGN, nq, 1, 1, True, True, True, FrequencyNotation.PH) + ref = deepcopy(a) + deepcopy(b) + a.add(b, copy=False) + assert np.array_equal(a.mat, ref.mat) + assert b.full_niw_range + + def test_add_with_localinteraction_and_interaction(rng): """A FourPoint adds a LocalInteraction and a momentum-dependent Interaction, preserving its shape.""" nq = (4, 4, 1) @@ -246,7 +356,7 @@ def test_sum_over_vn_reduces_dims(small_fourpoint): sl = fp.mat[0, 0, 0, 0, 0, 0, :, :, :] expect = (1 / beta) * np.sum(sl, axis=-1) got = out.mat[0, 0, 0, 0, 0, 0, :, :] - assert np.allclose(got, expect, rtol=1e-6, atol=1e-6) + assert np.allclose(got, expect, atol=1e-6) def test_sum_over_vn_raises_on_too_many_axes(small_fourpoint): @@ -565,7 +675,7 @@ def test_invert_and_sum_methods_agree_on_decompressed_fp(small_fourpoint): assert out1._num_vn_dimensions == 1 assert out2._num_vn_dimensions == 1 assert out1.current_shape == out2.current_shape - assert np.allclose(out1.mat, out2.mat, rtol=1e-6, atol=1e-8) + assert np.allclose(out1.mat, out2.mat, atol=1e-6) def test_invert_and_sum_methods_agree_on_compressed_fp(small_fourpoint_compressed): @@ -580,7 +690,7 @@ def test_invert_and_sum_methods_agree_on_compressed_fp(small_fourpoint_compresse assert out1._num_vn_dimensions == 1 assert out2._num_vn_dimensions == 1 assert out1.current_shape == out2.current_shape - assert np.allclose(out1.mat, out2.mat, rtol=1e-6, atol=1e-8) + assert np.allclose(out1.mat, out2.mat, atol=1e-6) def test_invert_and_sum_scales_with_beta(small_fourpoint_compressed): @@ -594,7 +704,7 @@ def test_invert_and_sum_scales_with_beta(small_fourpoint_compressed): out_beta2 = deepcopy(fp).invert_and_sum_over_last_vn_v2(beta2) scale = beta1 / beta2 - assert np.allclose(out_beta2.mat, out_beta1.mat * scale, rtol=1e-8, atol=1e-10) + assert np.allclose(out_beta2.mat, out_beta1.mat * scale, atol=1e-8) def test_invert_and_sum_v1_scales_with_beta(small_fourpoint_compressed): @@ -640,3 +750,98 @@ def test_invert_and_sum_and_invert_preserve_complex64(small_fourpoint_compressed assert deepcopy(fp).invert_and_sum_over_last_vn(2.0).mat.dtype == np.complex64 assert deepcopy(fp).invert_and_sum_over_last_vn_v2(2.0).mat.dtype == np.complex64 assert deepcopy(fp).invert().mat.dtype == np.complex64 + + +def _compound_product_reference_q(mat1: np.ndarray, mat2: np.ndarray, notation: FrequencyNotation) -> np.ndarray: + """Per-momentum compound-space matrix product of two full-index tensors [q,o,o,o,o,v,v'] in the given frequency + notation (ph: rows {1,2,v}, cols {4,3,v'}; pp: rows {1,3,v}, cols {4,2,v'}).""" + nq_tot, o, n2 = mat1.shape[0], mat1.shape[1], mat1.shape[-1] + dim = o * o * n2 + order = (0, 1, 4, 3, 2, 5) if notation == FrequencyNotation.PH else (0, 2, 4, 3, 1, 5) + order_q = (0,) + tuple(i + 1 for i in order) + compound_shape = (nq_tot,) + tuple(np.array(mat1.shape[1:])[list(order)]) + prod = np.transpose(mat1, order_q).reshape(nq_tot, dim, dim) @ np.transpose(mat2, order_q).reshape(nq_tot, dim, dim) + return np.transpose(prod.reshape(compound_shape), np.argsort(order_q)) + + +@pytest.mark.parametrize("notation", [FrequencyNotation.PH, FrequencyNotation.PP]) +def test_matmul_propagates_frequency_notation_and_compound_pairing(notation): + """Matmul contracts each momentum slice in the compound space of the operands' notation and the result carries + the frequency notation of self, so pp results unravel with the acbd back-permute.""" + rng = np.random.default_rng(13) + nq, o, niv = (2, 2, 1), 2, 3 + nq_tot = int(np.prod(nq)) + shape = (nq_tot, o, o, o, o, 1, 2 * niv, 2 * niv) + mat1 = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + mat2 = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + x = FourPoint(mat1.copy(), SpinChannel.DENS, nq, 1, 2, True, True, True, notation) + y = FourPoint(mat2.copy(), SpinChannel.DENS, nq, 1, 2, True, True, True, notation) + z = x @ y + ref = _compound_product_reference_q( + mat1[:, :, :, :, :, 0].astype(np.complex64), mat2[:, :, :, :, :, 0].astype(np.complex64), notation + ) + assert z.frequency_notation == notation + assert np.allclose(z.mat[:, :, :, :, :, 0], ref, atol=1e-4) + + +@pytest.mark.parametrize("notation", [FrequencyNotation.PH, FrequencyNotation.PP]) +def test_matmul_mixed_vn_respects_frequency_notation(notation): + """The memory-saving 2vn @ 1vn matmul branch contracts each momentum slice with the notation's orbital pairing + (the 1vn operand acts nu-diagonally on the result's second frequency) and keeps the frequency notation of + self.""" + rng = np.random.default_rng(16) + nq, o, niv = (2, 2, 1), 2, 3 + nq_tot = int(np.prod(nq)) + shape2 = (nq_tot, o, o, o, o, 1, 2 * niv, 2 * niv) + shape1 = (nq_tot, o, o, o, o, 1, 2 * niv) + mat2v = rng.standard_normal(shape2) + 1j * rng.standard_normal(shape2) + mat1v = rng.standard_normal(shape1) + 1j * rng.standard_normal(shape1) + x = FourPoint(mat2v.copy(), SpinChannel.DENS, nq, 1, 2, True, True, True, notation) + y = FourPoint(mat1v.copy(), SpinChannel.DENS, nq, 1, 1, True, True, True, notation) + z = x @ y + y_diag = np.zeros(shape2, dtype=np.complex64) + idx = np.arange(2 * niv) + y_diag[..., idx, idx] = mat1v + ref = _compound_product_reference_q( + mat2v[:, :, :, :, :, 0].astype(np.complex64), y_diag[:, :, :, :, :, 0], notation + ) + assert z.frequency_notation == notation + assert z.num_vn_dimensions == 2 + assert np.allclose(z.mat[:, :, :, :, :, 0], ref, atol=1e-4) + + +@pytest.mark.parametrize("notation", [FrequencyNotation.PH, FrequencyNotation.PP]) +def test_matmul_with_local_interaction_respects_frequency_notation(notation): + """FourPoint @ LocalInteraction contracts the frequency-constant bare interaction with the notation's orbital + pairing on every momentum slice and keeps the frequency notation of the four-point operand.""" + rng = np.random.default_rng(17) + nq, o, niv = (2, 2, 1), 2, 3 + nq_tot = int(np.prod(nq)) + shape = (nq_tot, o, o, o, o, 1, 2 * niv, 2 * niv) + mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + umat = rng.standard_normal((o, o, o, o)) + 1j * rng.standard_normal((o, o, o, o)) + x = FourPoint(mat.copy(), SpinChannel.DENS, nq, 1, 2, True, True, True, notation) + u = LocalInteraction(umat.copy()) + u_diag = np.zeros(shape, dtype=np.complex64) + idx = np.arange(2 * niv) + u_diag[..., idx, idx] = umat[None, :, :, :, :, None, None].astype(np.complex64) + z = x @ u + ref = _compound_product_reference_q(mat[:, :, :, :, :, 0].astype(np.complex64), u_diag[:, :, :, :, :, 0], notation) + assert z.frequency_notation == notation + assert np.allclose(z.mat[:, :, :, :, :, 0], ref, atol=1e-4) + + +def test_pow_pp_squares_in_pp_compound_space_without_explicit_identity(): + """fp ** 2 on a pp object squares each momentum slice in the pp compound space (rows {1,3,v}, cols {4,2,v'}) + and keeps the PP notation, with the matching identity derived internally via identity_like.""" + rng = np.random.default_rng(20) + nq, o, niv = (2, 2, 1), 2, 3 + nq_tot = int(np.prod(nq)) + shape = (nq_tot, o, o, o, o, 1, 2 * niv, 2 * niv) + mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + fp = FourPoint(mat.copy(), SpinChannel.DENS, nq, 1, 2, True, True, True, FrequencyNotation.PP) + result = fp**2 + mat64 = mat[:, :, :, :, :, 0].astype(np.complex64) + ref = _compound_product_reference_q(mat64, mat64, FrequencyNotation.PP) + assert result.frequency_notation == FrequencyNotation.PP + assert np.allclose(result.mat[:, :, :, :, :, 0], ref, atol=1e-4) diff --git a/tests/test_full_vertex_integration.py b/tests/test_full_vertex_integration.py new file mode 100644 index 00000000..ffeef7ae --- /dev/null +++ b/tests/test_full_vertex_integration.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2025-2026 Julian Peil +# SPDX-License-Identifier: MIT +# +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# Eliashberg Equation Solver for Strongly Correlated Electron Systems + +import os +from types import SimpleNamespace + +import numpy as np +import pytest + +from dgamore import config, eliashberg_solver, nonlocal_sde +from dgamore.dga_logger import DgaLogger +from dgamore.four_point import FourPoint +from dgamore.interaction import Interaction, LocalInteraction +from dgamore.local_four_point import LocalFourPoint +from dgamore.n_point_base import SpinChannel +from tests import conftest + +# captured at import time, before the autouse mock_numpy_save fixture patches np.save; the integration test needs +# real file round trips because create_full_vertex_q_r loads its intermediates from disk +_real_np_save = np.save + + +class _SingleRankDistributor: + """Minimal stand-in for MpiDistributor on one rank: identity gather/scatter, no-op barrier.""" + + def __init__(self): + self.comm = SimpleNamespace(rank=0, size=1) + self.my_rank = 0 + self.my_slice = slice(None) + + def barrier(self): + pass + + def gather(self, mat, root=0): + return mat + + def scatter(self, mat, root=0): + return mat + + +def _to_mat(x: np.ndarray, nv: int, no: int) -> np.ndarray: + """Maps a two-frequency four-orbital tensor X^{vv'}_{1234} to its compound-matrix representation + M[(v,1,2),(v',4,3)], under which the vertex product and inversion become plain matrix operations.""" + d = nv * no * no + return x.transpose(4, 0, 1, 5, 3, 2).reshape(d, d) + + +def _from_mat(m: np.ndarray, nv: int, no: int) -> np.ndarray: + """Inverse of :func:`_to_mat`.""" + return m.reshape(nv, no, no, nv, no, no).transpose(1, 2, 5, 4, 0, 3) + + +@pytest.fixture +def setup(tmp_path, monkeypatch): + monkeypatch.setattr(np, "save", _real_np_save) # restore real saving over the autouse mock + + comm_mock = conftest.create_comm_mock() + config.logger = DgaLogger(comm_mock, "./") + + config.sys.beta = 7.3 + config.box.niw_core = 2 + config.box.niv_core = 3 + config.output.output_path = str(tmp_path) + config.output.eliashberg_path = str(tmp_path) + config.eliashberg.perform_eliashberg = True + config.eliashberg.save_fq = True # keep the result in ph notation for the comparison + config.eliashberg.construct_fq_cheap = False + config.memory.save_memory_for_chiq_aux = False + config.lambda_correction.perform_lambda_correction = False + config.self_consistency.restrict_chi_phys = False + + yield tmp_path + + +@pytest.mark.parametrize("no", [2, 3, 4, 5]) +def test_create_full_vertex_q_r_matches_exact_bse_inversion(setup, no): + """The production F^q chain (calculate_sigma_kernel_r_q + create_full_vertex_q_r) reproduces + the exact BSE inversion.""" + rng = np.random.default_rng(0) + beta = config.sys.beta + n_q, n_w, niv = 2, 3, 3 + + # reversal-symmetric, invertible bubble slices: -beta (A(x)B + A^T(x)B^T) + chi0_mat = np.zeros((n_q, no, no, no, no, n_w, 2 * niv), complex) + for iq in range(n_q): + for iw in range(n_w): + for iv in range(2 * niv): + a = rng.standard_normal((no, no)) + 1j * rng.standard_normal((no, no)) + b = rng.standard_normal((no, no)) + 1j * rng.standard_normal((no, no)) + chi0_mat[iq, ..., iw, iv] = -beta * ( + np.einsum("il,kj->ijkl", a, b) + np.einsum("il,kj->ijkl", a.T, b.T) + ) + + gchi0_q = FourPoint(chi0_mat, SpinChannel.NONE, (n_q, 1, 1), 1, 1, False, True, True) + gchi0_q_inv = gchi0_q.invert() + gchi0_q_inv.save(name="gchi0_q_inv_rank_0", output_dir=config.output.eliashberg_path) + + # local irreducible vertex with the physical (nu, nu') time-reversal symmetry + gamma_mat = 0.3 * ( + rng.standard_normal((no, no, no, no, n_w, 2 * niv, 2 * niv)) + + 1j * rng.standard_normal((no, no, no, no, n_w, 2 * niv, 2 * niv)) + ) + gamma_r = LocalFourPoint(gamma_mat, SpinChannel.DENS, 1, 2, full_niw_range=False).symmetrize_v_vp() + + # reversal-symmetric local interaction, vanishing non-local interaction + u_mat = rng.standard_normal((no, no, no, no)) + u_mat = 0.5 * (u_mat + u_mat.transpose(1, 0, 3, 2)) + u_mat = 0.5 * (u_mat + u_mat.transpose(3, 2, 1, 0)) + u_loc = LocalInteraction(0.5 * u_mat) + v_nonloc = Interaction(np.zeros((n_q, no, no, no, no)), SpinChannel.NONE, (n_q, 1, 1), True) + + u_r = u_loc.as_channel(SpinChannel.DENS).mat + assert np.allclose(u_r, u_r.transpose(3, 2, 1, 0)) # precondition: TR-symmetric projected interaction + + # zero shell terms: the dressing reduces to [(sum chi*)^{-1} + U_r]^{-1} + zero_sum = FourPoint(np.zeros((n_q, no, no, no, no, n_w)), SpinChannel.NONE, (n_q, 1, 1), 1, 0, False, True, True) + + dist = _SingleRankDistributor() + nonlocal_sde.calculate_sigma_kernel_r_q(gamma_r, gchi0_q_inv, zero_sum, zero_sum, u_loc, v_nonloc, dist) + + f_q = eliashberg_solver.create_full_vertex_q_r(u_loc, v_nonloc, gamma_r, niv, dist) + + # exact reference per (q, omega) slice from the raw toy tensors: F depends only on chi0, Gamma and beta + for iq in range(n_q): + for iw in range(n_w): + m_chi0 = np.zeros((2 * niv * no * no, 2 * niv * no * no), complex) + blk = no * no + for iv in range(2 * niv): + m_chi0[iv * blk : (iv + 1) * blk, iv * blk : (iv + 1) * blk] = ( + chi0_mat[iq, ..., iw, iv].transpose(0, 1, 3, 2).reshape(blk, blk) + ) + m_chi0_inv = np.linalg.inv(m_chi0) + m_gamma = _to_mat(gamma_r.mat[..., iw, :, :], 2 * niv, no) + m_chi = np.linalg.inv(m_chi0_inv + m_gamma / beta**2) + f_exact = _from_mat(beta**2 * (m_chi0_inv - m_chi0_inv @ m_chi @ m_chi0_inv), 2 * niv, no) + + f_code = f_q.mat[iq, :, :, :, :, iw, :, :] + scale = np.max(np.abs(f_exact)) + assert np.max(np.abs(f_code - f_exact)) / scale < 1e-4, f"F^q mismatch at q={iq}, w={iw}" diff --git a/tests/test_greens_function.py b/tests/test_greens_function.py index e9b15786..bc29f9bc 100644 --- a/tests/test_greens_function.py +++ b/tests/test_greens_function.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems from unittest.mock import MagicMock @@ -234,7 +234,7 @@ def test_get_epot_e_corr_einsum_matches_transposed_product_reference(): dsigma = g._sigma.decompress_q_dimension().mat - smom0[..., None] e_corr_ref = (dsigma * g.decompress_q_dimension().transpose_orbitals().mat).sum().real / beta e_corr_new = np.einsum("...abv,...bav->...", dsigma, g.decompress_q_dimension().mat).sum().real / beta - assert np.allclose(e_corr_new, e_corr_ref, rtol=1e-5) + assert np.allclose(e_corr_new, e_corr_ref, atol=1e-5) assert np.isfinite(g.get_epot()) diff --git a/tests/test_hamiltonian.py b/tests/test_hamiltonian.py index ea56ee69..c2ed321b 100644 --- a/tests/test_hamiltonian.py +++ b/tests/test_hamiltonian.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import os diff --git a/tests/test_interaction.py b/tests/test_interaction.py index 5cadd282..9ffc9f76 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import pytest @@ -18,7 +18,7 @@ def test_localinteraction_adds_correctly(): interaction1 = LocalInteraction(mat1) interaction2 = LocalInteraction(mat2) result = interaction1 + interaction2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) def test_localinteraction_handles_identity_permutation(): @@ -26,7 +26,7 @@ def test_localinteraction_handles_identity_permutation(): mat = np.array([[1, 2], [3, 4]]) interaction = LocalInteraction(mat) result = interaction.permute_orbitals("abcd->abcd") - assert np.allclose(result.mat, mat, rtol=1e-2) + assert np.allclose(result.mat, mat, atol=1e-2) def test_localinteraction_raises_error_on_invalid_permutation(): @@ -58,7 +58,7 @@ def test_transforms_to_correct_channel(channel, expected_mat): """LocalInteraction.as_channel builds the dens/magn/sing/trip combinations.""" interaction = LocalInteraction(u_loc, SpinChannel.NONE) result = interaction.as_channel(channel) - assert np.allclose(result.mat, expected_mat, rtol=1e-2) + assert np.allclose(result.mat, expected_mat, atol=1e-2) assert result.channel == channel @@ -67,7 +67,7 @@ def test_interaction_exponentiates_correctly(): mat = np.array(np.random.rand(2, 2, 2, 2)) interaction = LocalInteraction(mat) result = interaction**2 - assert np.allclose(result.mat, np.einsum("abcd,dcef->abef", mat, mat, optimize=True), rtol=1e-2) + assert np.allclose(result.mat, np.einsum("abcd,dcef->abef", mat, mat, optimize=True), atol=1e-2) def test_interaction_raises_error_on_invalid_exponentiation(): @@ -92,7 +92,7 @@ def test_localinteraction_rsub_has_correct_sign(): u = LocalInteraction(mat) other = np.zeros_like(mat) result = u.__rsub__(other) # directly call __rsub__ to test B - A = C - assert np.allclose(result.mat, -mat, rtol=1e-2) + assert np.allclose(result.mat, -mat, atol=1e-2) def test_interaction_handles_channel_transformation(): @@ -100,7 +100,7 @@ def test_interaction_handles_channel_transformation(): mat = np.array([[1, 2], [3, 4]]) interaction = Interaction(mat, SpinChannel.NONE) result = interaction.as_channel(SpinChannel.DENS) - assert np.allclose(result.mat, 2 * mat, rtol=1e-2) + assert np.allclose(result.mat, 2 * mat, atol=1e-2) def test_interaction_raises_error_on_invalid_channel_transformation(): @@ -133,7 +133,7 @@ def test_permute_orbitals_returns_same_object_for_identity_permutation(n): mat = np.random.rand(16, n, n, n, n) interaction = Interaction(mat) result = interaction.permute_orbitals("abcd->abcd") - assert np.allclose(result.mat, interaction.mat, rtol=1e-2) + assert np.allclose(result.mat, interaction.mat, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -143,7 +143,7 @@ def test_permute_orbitals_applies_correct_permutation_with_compressed_q_dimensio interaction = Interaction(mat, has_compressed_q_dimension=True) result = interaction.permute_orbitals("abcd->adcb") expected = np.einsum("...abcd->...adcb", mat, optimize=True) - assert np.allclose(result.mat, expected, rtol=1e-2) + assert np.allclose(result.mat, expected, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -153,7 +153,7 @@ def test_permute_orbitals_applies_correct_permutation_with_decompressed_q_dimens interaction = Interaction(mat, has_compressed_q_dimension=False) result = interaction.permute_orbitals("abcd->adcb") expected = np.einsum("...abcd->...adcb", mat, optimize=True) - assert np.allclose(result.mat, expected, rtol=1e-2) + assert np.allclose(result.mat, expected, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -179,7 +179,7 @@ def test_interaction_handles_compressed_q_dimension_exponentiation(): interaction = Interaction(mat, has_compressed_q_dimension=True) result = interaction**2 assert result.mat.shape == mat.shape - assert np.allclose(result.mat, np.einsum("qabcd,qdcef->qabef", mat, mat, optimize=True), rtol=1e-2) + assert np.allclose(result.mat, np.einsum("qabcd,qdcef->qabef", mat, mat, optimize=True), atol=1e-2) def test_raises_error_when_exponentiating_with_invalid_power(): @@ -196,7 +196,7 @@ def test_transforms_to_dens_channel_correctly(n): mat = np.random.rand(4, n, n, n, n) interaction = Interaction(mat, SpinChannel.NONE) result = interaction.as_channel(SpinChannel.DENS) - assert np.allclose(result.mat, 2 * interaction.mat, rtol=1e-2) + assert np.allclose(result.mat, 2 * interaction.mat, atol=1e-2) assert result.channel == SpinChannel.DENS @@ -206,7 +206,7 @@ def test_transforms_to_magn_channel_correctly(n): mat = np.random.rand(4, n, n, n, n) interaction = Interaction(mat, SpinChannel.NONE) result = interaction.as_channel(SpinChannel.MAGN) - assert np.allclose(result.mat, 0 * interaction.mat, rtol=1e-2) + assert np.allclose(result.mat, 0 * interaction.mat, atol=1e-2) assert result.channel == SpinChannel.MAGN @@ -216,7 +216,7 @@ def test_transforms_to_sing_channel_correctly(n): mat = np.random.rand(4, n, n, n, n) interaction = Interaction(mat, SpinChannel.NONE) result = interaction.as_channel(SpinChannel.SING) - assert np.allclose(result.mat, interaction.mat, rtol=1e-2) + assert np.allclose(result.mat, interaction.mat, atol=1e-2) assert result.channel == SpinChannel.SING @@ -226,7 +226,7 @@ def test_transforms_to_trip_channel_correctly(n): mat = np.random.rand(4, n, n, n, n) interaction = Interaction(mat, SpinChannel.NONE) result = interaction.as_channel(SpinChannel.TRIP) - assert np.allclose(result.mat, interaction.mat, rtol=1e-2) + assert np.allclose(result.mat, interaction.mat, atol=1e-2) assert result.channel == SpinChannel.TRIP @@ -246,7 +246,7 @@ def test_adds_interaction_with_numpy_array_correctly(n): mat2 = np.random.rand(16, n, n, n, n) interaction = Interaction(mat1, has_compressed_q_dimension=True) result = interaction + mat2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -257,7 +257,7 @@ def test_adds_two_interactions_correctly_1(n): interaction1 = Interaction(mat1, has_compressed_q_dimension=True) interaction2 = Interaction(mat2, has_compressed_q_dimension=True) result = interaction1 + interaction2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -268,7 +268,7 @@ def test_adds_two_interactions_correctly_2(n): interaction1 = Interaction(mat1, has_compressed_q_dimension=True) interaction2 = Interaction(mat2, has_compressed_q_dimension=False) result = interaction1 + interaction2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -279,7 +279,7 @@ def test_adds_two_interactions_correctly_3(n): interaction1 = Interaction(mat1, has_compressed_q_dimension=False) interaction2 = Interaction(mat2, has_compressed_q_dimension=True) result = interaction1 + interaction2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -291,7 +291,7 @@ def test_adds_interaction_with_localinteraction_correctly_if_decompressed(n): local_interaction = LocalInteraction(mat2) result = interaction + local_interaction expected = mat1 + mat2[None, ...] - assert np.allclose(result.mat, expected, rtol=1e-2) + assert np.allclose(result.mat, expected, atol=1e-2) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -303,7 +303,7 @@ def test_adds_interaction_with_localinteraction_correctly_if_compressed(n): local_interaction = LocalInteraction(mat2) result = interaction + local_interaction expected = mat1 + mat2[None, ...] - assert np.allclose(result.mat, expected, rtol=1e-2) + assert np.allclose(result.mat, expected, atol=1e-2) @pytest.mark.parametrize("n", [1, 2]) @@ -319,7 +319,7 @@ def test_adds_localinteraction_on_left_with_interaction_returns_interaction(n): assert isinstance(result, Interaction) assert result.has_compressed_q_dimension assert result.current_shape == v_mat.shape - assert np.allclose(result.mat, u_mat[None, ...] + v_mat, rtol=1e-2) + assert np.allclose(result.mat, u_mat[None, ...] + v_mat, atol=1e-2) @pytest.mark.parametrize("n", [1, 2]) @@ -335,7 +335,7 @@ def test_subtracts_interaction_from_localinteraction_on_left_returns_interaction assert isinstance(result, Interaction) assert result.has_compressed_q_dimension assert result.current_shape == v_mat.shape - assert np.allclose(result.mat, u_mat[None, ...] - v_mat, rtol=1e-2) + assert np.allclose(result.mat, u_mat[None, ...] - v_mat, atol=1e-2) def test_adds_two_interactions_using_operator_correctly(): @@ -345,7 +345,7 @@ def test_adds_two_interactions_using_operator_correctly(): interaction1 = Interaction(mat1) interaction2 = Interaction(mat2) result = interaction1 + interaction2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) def test_adds_interaction_and_numpy_array_using_operator_correctly(): @@ -354,7 +354,7 @@ def test_adds_interaction_and_numpy_array_using_operator_correctly(): mat2 = np.random.rand(4, 4, 2, 2) interaction = Interaction(mat1) result = interaction + mat2 - assert np.allclose(result.mat, mat1 + mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 + mat2, atol=1e-2) def test_subtracts_two_interactions_using_operator_correctly(): @@ -364,7 +364,7 @@ def test_subtracts_two_interactions_using_operator_correctly(): interaction1 = Interaction(mat1) interaction2 = Interaction(mat2) result = interaction1 - interaction2 - assert np.allclose(result.mat, mat1 - mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 - mat2, atol=1e-2) def test_subtracts_interaction_and_numpy_array_using_operator_correctly(): @@ -373,7 +373,7 @@ def test_subtracts_interaction_and_numpy_array_using_operator_correctly(): mat2 = np.random.rand(4, 4, 2, 2) interaction = Interaction(mat1) result = interaction - mat2 - assert np.allclose(result.mat, mat1 - mat2, rtol=1e-2) + assert np.allclose(result.mat, mat1 - mat2, atol=1e-2) def test_raises_error_when_adding_unsupported_type(): @@ -390,4 +390,4 @@ def test_nonlocal_interaction_rsub_has_correct_sign(): v = Interaction(mat, SpinChannel.NONE, (1, 1, 1), has_compressed_q_dimension=True) other = np.zeros_like(mat) result = v.__rsub__(other) # directly call __rsub__ to test B - A = C - assert np.allclose(result.mat, -mat, rtol=1e-2) + assert np.allclose(result.mat, -mat, atol=1e-2) diff --git a/tests/test_lambda_correction.py b/tests/test_lambda_correction.py index 76a0f571..71fda4f3 100644 --- a/tests/test_lambda_correction.py +++ b/tests/test_lambda_correction.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import os diff --git a/tests/test_local_four_point.py b/tests/test_local_four_point.py index 10a01a0c..9d744bb0 100644 --- a/tests/test_local_four_point.py +++ b/tests/test_local_four_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems from unittest.mock import patch, MagicMock @@ -12,7 +12,7 @@ from dgamore.local_four_point import LocalFourPoint import numpy as np -from dgamore.n_point_base import SpinChannel +from dgamore.n_point_base import FrequencyNotation, SpinChannel @pytest.mark.parametrize("n", [1, 2, 3]) @@ -25,7 +25,7 @@ def test_exponentiation_with_positive_power_1(n): expected = obj for _ in range(n - 1): expected = expected @ obj - assert np.allclose(result.mat, expected.mat, rtol=1e-4) + assert np.allclose(result.mat, expected.mat, atol=1e-4) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -38,7 +38,7 @@ def test_exponentiation_with_positive_power_2(n): expected = obj for _ in range(n - 1): expected = expected @ obj - assert np.allclose(result.mat, expected.mat, rtol=1e-4) + assert np.allclose(result.mat, expected.mat, atol=1e-4) def test_exponentiation_with_zero_power_returns_identity_1(): @@ -47,7 +47,7 @@ def test_exponentiation_with_zero_power_returns_identity_1(): obj = LocalFourPoint(mat, channel=SpinChannel.NONE, num_vn_dimensions=1) identity = LocalFourPoint.identity_like(obj) result = obj.pow(0, identity) - assert np.allclose(result.mat, identity.mat, rtol=1e-4) + assert np.allclose(result.mat, identity.mat, atol=1e-4) def test_exponentiation_with_zero_power_returns_identity_2(): @@ -56,7 +56,7 @@ def test_exponentiation_with_zero_power_returns_identity_2(): obj = LocalFourPoint(mat, channel=SpinChannel.NONE, num_vn_dimensions=2) identity = LocalFourPoint.identity_like(obj) result = obj.pow(0, identity) - assert np.allclose(result.mat, identity.mat, rtol=1e-4) + assert np.allclose(result.mat, identity.mat, atol=1e-4) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -69,7 +69,7 @@ def test_exponentiation_with_negative_power_1(n): expected = obj.invert() for _ in range(n - 1): expected = expected @ obj.invert() - assert np.allclose(result.mat, expected.mat, rtol=1e-4) + assert np.allclose(result.mat, expected.mat, atol=1e-4) @pytest.mark.parametrize("n", [1, 2, 3]) @@ -82,7 +82,7 @@ def test_exponentiation_with_negative_power_2(n): expected = obj.invert() for _ in range(n - 1): expected = expected @ obj.invert() - assert np.allclose(result.mat, expected.mat, rtol=1e-2) + assert np.allclose(result.mat, expected.mat, atol=1e-2) def test_exponentiation_with_non_integer_power_raises_error(): @@ -100,7 +100,7 @@ def test_symmetrizes_square_matrix_correctly(): obj = LocalFourPoint(mat) result = obj.symmetrize_v_vp() expected = np.array([[[[[[1, 2.5], [2.5, 4]]]]]]) - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_symmetrizes_random_matrix_correctly(): @@ -109,7 +109,7 @@ def test_symmetrizes_random_matrix_correctly(): obj = LocalFourPoint(mat) result = obj.symmetrize_v_vp() expected = 0.5 * (mat + mat.swapaxes(0, 3).swapaxes(1, 2).swapaxes(-1, -2)) - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_take_first_wn_selects_first_entry_and_returns_independent_copy(): @@ -119,7 +119,7 @@ def test_take_first_wn_selects_first_entry_and_returns_independent_copy(): expected = obj.mat[..., 0, :, :].copy() result = obj.take_first_wn() assert result.num_wn_dimensions == 0 - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) result.mat[0, 0, 0, 0, 0, 0] = 12345.0 assert not np.allclose(obj.mat[0, 0, 0, 0, 0, 0, 0], 12345.0) @@ -129,7 +129,7 @@ def test_handles_symmetric_matrix_without_modification(): mat = np.array([[[[[[1, 2], [2, 4]]]]]]) obj = LocalFourPoint(mat) result = obj.symmetrize_v_vp() - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) def test_raises_error_for_non_square_last_two_axes(): @@ -154,7 +154,7 @@ def test_sums_over_orbitals_correctly_1(): obj = LocalFourPoint(mat) result = obj.sum_over_orbitals("abcd->ad") assert result.mat.shape == (2, 2, 5, 3) - assert np.allclose(result.mat, np.sum(mat, axis=(1, 2)), rtol=1e-4) + assert np.allclose(result.mat, np.sum(mat, axis=(1, 2)), atol=1e-4) def test_sums_over_orbitals_correctly_2(): @@ -163,7 +163,7 @@ def test_sums_over_orbitals_correctly_2(): obj = LocalFourPoint(mat) result = obj.sum_over_orbitals("abcd->ad") assert result.mat.shape == (2, 2, 5, 3, 3) - assert np.allclose(result.mat, np.sum(mat, axis=(1, 2)), rtol=1e-4) + assert np.allclose(result.mat, np.sum(mat, axis=(1, 2)), atol=1e-4) def test_raises_error_for_invalid_orbital_contraction_format(): @@ -179,7 +179,7 @@ def test_handles_no_orbital_reduction(): mat = np.random.rand(2, 2, 2, 2, 5, 3, 3) obj = LocalFourPoint(mat) result = obj.sum_over_orbitals("abcd->abcd") - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) def test_reduces_orbital_dimensions_correctly_1(): @@ -188,7 +188,7 @@ def test_reduces_orbital_dimensions_correctly_1(): obj = LocalFourPoint(mat) result = obj.sum_over_orbitals("abcd->a") assert result.mat.shape == (3, 5, 4, 4) - assert np.allclose(result.mat, np.sum(mat, axis=(1, 2, 3)), rtol=1e-4) + assert np.allclose(result.mat, np.sum(mat, axis=(1, 2, 3)), atol=1e-4) def test_reduces_orbital_dimensions_correctly_2(): @@ -197,7 +197,7 @@ def test_reduces_orbital_dimensions_correctly_2(): obj = LocalFourPoint(mat) result = obj.sum_over_orbitals("abcd->ab") assert result.mat.shape == (3, 3, 5, 4, 4) - assert np.allclose(result.mat, np.sum(mat, axis=(2, 3)), rtol=1e-4) + assert np.allclose(result.mat, np.sum(mat, axis=(2, 3)), atol=1e-4) def test_reduces_orbital_dimensions_correctly_3(): @@ -206,7 +206,7 @@ def test_reduces_orbital_dimensions_correctly_3(): obj = LocalFourPoint(mat) result = obj.sum_over_orbitals("abcd->abc") assert result.mat.shape == (3, 3, 3, 5, 4, 4) - assert np.allclose(result.mat, np.sum(mat, axis=(3,)), rtol=1e-4) + assert np.allclose(result.mat, np.sum(mat, axis=(3,)), atol=1e-4) def test_sums_over_single_vn_dimension_correctly_1(): @@ -216,7 +216,7 @@ def test_sums_over_single_vn_dimension_correctly_1(): beta = 10.0 result = obj.sum_over_vn(beta, axis=(-1,)) expected_mat = 1 / beta * np.sum(mat, axis=-1) - assert np.allclose(result.mat, expected_mat, rtol=1e-4) + assert np.allclose(result.mat, expected_mat, atol=1e-4) assert result.num_vn_dimensions == 0 @@ -228,7 +228,7 @@ def test_sums_over_single_vn_dimension_correctly_2(n): beta = 10.0 result = obj.sum_over_vn(beta, axis=(-n,)) expected_mat = 1 / beta * np.sum(mat, axis=(-n,)) - assert np.allclose(result.mat, expected_mat, rtol=1e-4) + assert np.allclose(result.mat, expected_mat, atol=1e-4) assert result.num_vn_dimensions == 1 @@ -239,7 +239,7 @@ def test_sums_over_multiple_vn_dimensions_correctly(): beta = 10.0 result = obj.sum_over_vn(beta, axis=(-2, -1)) expected_mat = 1 / beta**2 * np.sum(mat, axis=(-2, -1)) - assert np.allclose(result.mat, expected_mat, rtol=1e-4) + assert np.allclose(result.mat, expected_mat, atol=1e-4) assert result.num_vn_dimensions == 0 @@ -259,7 +259,7 @@ def test_sums_over_all_vn_with_double_vn_dimensions_correctly(): beta = 10.0 result = obj.sum_over_all_vn(beta) expected_mat = 1 / beta**2 * np.sum(mat, axis=(-2, -1)) - assert np.allclose(result.mat, expected_mat, rtol=1e-4) + assert np.allclose(result.mat, expected_mat, atol=1e-4) assert result.num_vn_dimensions == 0 @@ -270,7 +270,7 @@ def test_sums_over_all_vn_with_single_vn_dimension_correctly(): beta = 10.0 result = obj.sum_over_all_vn(beta) expected_mat = 1 / beta * np.sum(mat, axis=-1) - assert np.allclose(result.mat, expected_mat, rtol=1e-4) + assert np.allclose(result.mat, expected_mat, atol=1e-4) assert result.num_vn_dimensions == 0 @@ -280,7 +280,7 @@ def test_handles_no_vn_dimensions_without_modification_for_sum(): obj = LocalFourPoint(mat, num_vn_dimensions=0) beta = 10.0 result = obj.sum_over_all_vn(beta) - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) assert result.num_vn_dimensions == 0 @@ -291,7 +291,7 @@ def test_contracts_legs_correctly_with_two_vn_dimensions(): beta = 10.0 result = obj.contract_legs(beta) assert result.mat.shape == (2, 2, 5) - assert np.allclose(result.mat, 1.0 / beta**2 * np.einsum("abcdefg->ade", mat), rtol=1e-4) + assert np.allclose(result.mat, 1.0 / beta**2 * np.einsum("abcdefg->ade", mat), atol=1e-4) def test_raises_error_when_contracting_legs_with_invalid_vn_dimensions(): @@ -328,7 +328,7 @@ def test_converts_to_compound_indices_with_no_vn_dimensions(): obj = LocalFourPoint(mat, num_vn_dimensions=0) result = obj.to_compound_indices() assert result.mat.shape == (5, 4, 4) - assert np.allclose(result.mat, mat.transpose(4, 0, 1, 3, 2).reshape(5, 4, 4), rtol=1e-4) + assert np.allclose(result.mat, mat.transpose(4, 0, 1, 3, 2).reshape(5, 4, 4), atol=1e-4) def test_converts_to_compound_indices_with_one_vn_dimension(): @@ -356,7 +356,7 @@ def test_converts_to_compound_indices_with_two_vn_dimensions(): mat = np.random.rand(2, 2, 2, 2, 5, 4, 4) obj = LocalFourPoint(mat, num_vn_dimensions=2) result = obj.to_compound_indices() - assert np.allclose(result.mat, mat.transpose(4, 0, 1, 5, 3, 2, 6).reshape(5, 16, 16), rtol=1e-4) + assert np.allclose(result.mat, mat.transpose(4, 0, 1, 5, 3, 2, 6).reshape(5, 16, 16), atol=1e-4) def test_raises_error_for_missing_bosonic_frequencies(): @@ -372,7 +372,7 @@ def test_handles_already_compound_indices_without_modification(): mat = np.random.rand(5, 4, 4) obj = LocalFourPoint(mat, num_wn_dimensions=1, num_vn_dimensions=2) result = obj.to_compound_indices() - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) @pytest.mark.parametrize( @@ -398,7 +398,7 @@ def test_converts_compound_indices_to_full_indices_correctly_for_one_vn_dimensio result = obj.to_full_indices() assert result.mat.shape == (2, 2, 2, 2, 5, 4, 4) assert result.num_vn_dimensions == 2 - assert np.allclose(mat, result.take_vn_diagonal().mat, rtol=1e-4) + assert np.allclose(mat, result.take_vn_diagonal().mat, atol=1e-4) def test_raises_error_for_invalid_current_shape(): @@ -426,7 +426,7 @@ def test_returns_original_object_when_already_in_full_indices(num_wn_dimensions, obj = LocalFourPoint(mat, num_wn_dimensions=num_wn_dimensions, num_vn_dimensions=num_vn_dimensions) result = obj.to_full_indices() assert result.mat.shape == shape - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) assert result.num_wn_dimensions == num_wn_dimensions assert result.num_vn_dimensions == num_vn_dimensions @@ -440,7 +440,7 @@ def test_handles_diagonal_extraction_for_single_vn_dimension(): assert result.mat.shape == (2, 2, 2, 2, 5, 4) mat = mat.reshape((5,) + (2, 2, 4) * 2).transpose(1, 2, 5, 4, 0, 3, 6).diagonal(axis1=-2, axis2=-1) - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) def test_raises_error_for_invalid_bosonic_frequency_dimensions(): @@ -508,8 +508,8 @@ def test_multiplies_two_objects_with_no_vn_dimensions_correctly(): result2 = obj2 @ obj1 expected1 = np.einsum("abcdw,dcefw->abefw", mat1, mat2, optimize=True) expected2 = np.einsum("abcdw,dcefw->abefw", mat2, mat1, optimize=True) - assert np.allclose(result1.mat, expected1[..., 2:], rtol=1e-4) - assert np.allclose(result2.mat, expected2[..., 2:], rtol=1e-4) + assert np.allclose(result1.mat, expected1[..., 2:], atol=1e-4) + assert np.allclose(result2.mat, expected2[..., 2:], atol=1e-4) def test_multiplies_two_objects_with_one_vn_dimension_correctly(): @@ -522,8 +522,8 @@ def test_multiplies_two_objects_with_one_vn_dimension_correctly(): result2 = obj2 @ obj1 expected1 = np.einsum("abcdwv,dcefwv->abefwv", mat1, mat2, optimize=True) expected2 = np.einsum("abcdwv,dcefwv->abefwv", mat2, mat1, optimize=True) - assert np.allclose(result1.mat, expected1[..., 2:, :], rtol=1e-4) - assert np.allclose(result2.mat, expected2[..., 2:, :], rtol=1e-4) + assert np.allclose(result1.mat, expected1[..., 2:, :], atol=1e-4) + assert np.allclose(result2.mat, expected2[..., 2:, :], atol=1e-4) @pytest.mark.parametrize( @@ -577,8 +577,8 @@ def test_handles_multiplication_with_local_interaction_correctly(): result2 = obj2 @ obj1 expected1 = np.einsum("abcdwvp,dcef->abefwvp", mat1, mat2, optimize=True) expected2 = np.einsum("abcd,dcefwvp->abefwvp", mat2, mat1, optimize=True) - assert np.allclose(result1.mat, expected1, rtol=1e-4) - assert np.allclose(result2.mat, expected2, rtol=1e-4) + assert np.allclose(result1.mat, expected1, atol=1e-4) + assert np.allclose(result2.mat, expected2, atol=1e-4) def test_multiplies_objects_with_mixed_vn_dimensions_correctly(): @@ -591,8 +591,8 @@ def test_multiplies_objects_with_mixed_vn_dimensions_correctly(): result2 = obj2 @ obj1 expected1 = np.einsum("abcdwv,dcefw->abefwv", mat1, mat2, optimize=True) expected2 = np.einsum("abcdw,dcefwv->abefwv", mat2, mat1, optimize=True) - assert np.allclose(result1.mat, expected1[..., 2:, :], rtol=1e-4) - assert np.allclose(result2.mat, expected2[..., 2:, :], rtol=1e-4) + assert np.allclose(result1.mat, expected1[..., 2:, :], atol=1e-4) + assert np.allclose(result2.mat, expected2[..., 2:, :], atol=1e-4) assert result1.num_vn_dimensions == 1 assert result2.num_vn_dimensions == 1 @@ -615,7 +615,7 @@ def test_multiplies_with_scalar_correctly(): scalar = 2.5 result = obj * scalar expected = mat * scalar - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_multiplies_with_numpy_array_correctly(): @@ -625,7 +625,7 @@ def test_multiplies_with_numpy_array_correctly(): array = np.random.rand(2, 2, 2, 2, 5, 4) result = obj * array expected = mat * array - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_raises_error_for_invalid_multiplication_type(): @@ -662,7 +662,7 @@ def test_multiplies_two_objects_with_one_vn_dimension_and_generates_two_vn_dimen obj2 = LocalFourPoint(mat2, num_vn_dimensions=1, full_niw_range=True) result = obj1 * obj2 expected = np.einsum("abcdwv,dcefwp->abefwvp", mat1, mat2, optimize=True) - assert np.allclose(result.mat, expected[..., 10:, :, :], rtol=1e-4) + assert np.allclose(result.mat, expected[..., 10:, :, :], atol=1e-4) def test_converts_to_half_bosonic_range_correctly_1(): @@ -672,7 +672,7 @@ def test_converts_to_half_bosonic_range_correctly_1(): result = obj.to_half_niw_range() assert result is obj assert result.mat.shape == (2, 2, 2, 2, 11) - assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-1), rtol=1e-4) + assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-1), atol=1e-4) def test_converts_to_half_bosonic_range_correctly_2(): @@ -682,7 +682,7 @@ def test_converts_to_half_bosonic_range_correctly_2(): result = obj.to_half_niw_range() assert result is obj assert result.mat.shape == (2, 2, 2, 2, 11, 20) - assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-2), rtol=1e-4) + assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-2), atol=1e-4) def test_converts_to_half_bosonic_range_correctly_3(): @@ -692,7 +692,7 @@ def test_converts_to_half_bosonic_range_correctly_3(): result = obj.to_half_niw_range() assert result is obj assert result.mat.shape == (2, 2, 2, 2, 11, 10, 10) - assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-3), rtol=1e-4) + assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-3), atol=1e-4) def test_to_full_niw_range_to_half_niw_range_should_reproduce_original_1(): @@ -700,7 +700,7 @@ def test_to_full_niw_range_to_half_niw_range_should_reproduce_original_1(): mat = np.random.rand(2, 2, 2, 2, 21) + 1j * np.random.rand(2, 2, 2, 2, 21) obj = LocalFourPoint(mat, num_vn_dimensions=0, full_niw_range=False) obj = obj.to_full_niw_range().to_half_niw_range() - assert np.allclose(obj.mat, mat, rtol=1e-4) + assert np.allclose(obj.mat, mat, atol=1e-4) assert obj.full_niw_range is False assert obj.num_vn_dimensions == 0 @@ -710,7 +710,7 @@ def test_to_full_niw_range_to_half_niw_range_should_reproduce_original_2(): mat = np.random.rand(2, 2, 2, 2, 21, 4) + 1j * np.random.rand(2, 2, 2, 2, 21, 4) obj = LocalFourPoint(mat, num_vn_dimensions=1, full_niw_range=False) obj = obj.to_full_niw_range().to_half_niw_range() - assert np.allclose(obj.mat, mat, rtol=1e-4) + assert np.allclose(obj.mat, mat, atol=1e-4) assert obj.full_niw_range is False assert obj.num_vn_dimensions == 1 @@ -720,7 +720,7 @@ def test_to_full_niw_range_to_half_niw_range_should_reproduce_original_3(): mat = np.random.rand(2, 2, 2, 2, 21, 4, 4) + 1j * np.random.rand(2, 2, 2, 2, 21, 4, 4) obj = LocalFourPoint(mat, num_vn_dimensions=2, full_niw_range=False) obj = obj.to_full_niw_range().to_half_niw_range() - assert np.allclose(obj.mat, mat, rtol=1e-4) + assert np.allclose(obj.mat, mat, atol=1e-4) assert obj.full_niw_range is False assert obj.num_vn_dimensions == 2 @@ -734,7 +734,7 @@ def test_adds_two_local_four_point_objects_correctly(): result = obj1 + obj2 expected = mat1 + mat2 assert result.full_niw_range == False - assert np.allclose(result.mat, expected[..., 10:, :, :], rtol=1e-4) + assert np.allclose(result.mat, expected[..., 10:, :, :], atol=1e-4) def test_adds_two_local_four_point_objects_with_different_vn_dimensions(): @@ -772,12 +772,12 @@ def test_adds_two_local_four_point_objects_with_different_vn_dimensions(): assert result5.full_niw_range is False assert result6.full_niw_range is False - assert np.allclose(result1.mat, (mat1 + mat2_diagonal)[..., 10:, :, :], rtol=1e-4) - assert np.allclose(result2.mat, (mat1 + mat3[..., None, None])[..., 10:, :, :], rtol=1e-4) - assert np.allclose(result3.mat, (mat2 + mat3[..., None])[..., 10:, :], rtol=1e-4) - assert np.allclose(result4.mat, (mat2 + mat2)[..., 10:, :], rtol=1e-4) - assert np.allclose(result5.mat, (mat3 + mat3)[..., 10:], rtol=1e-4) - assert np.allclose(result6.mat, (mat1 + mat1)[..., 10:, :, :], rtol=1e-4) + assert np.allclose(result1.mat, (mat1 + mat2_diagonal)[..., 10:, :, :], atol=1e-4) + assert np.allclose(result2.mat, (mat1 + mat3[..., None, None])[..., 10:, :, :], atol=1e-4) + assert np.allclose(result3.mat, (mat2 + mat3[..., None])[..., 10:, :], atol=1e-4) + assert np.allclose(result4.mat, (mat2 + mat2)[..., 10:, :], atol=1e-4) + assert np.allclose(result5.mat, (mat3 + mat3)[..., 10:], atol=1e-4) + assert np.allclose(result6.mat, (mat1 + mat1)[..., 10:, :, :], atol=1e-4) def test_adds_local_four_point_and_scalar_correctly(): @@ -787,7 +787,7 @@ def test_adds_local_four_point_and_scalar_correctly(): obj = LocalFourPoint(mat, num_vn_dimensions=2) result = obj + scalar expected = mat + scalar - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_adds_local_four_point_and_numpy_array_correctly(): @@ -797,7 +797,7 @@ def test_adds_local_four_point_and_numpy_array_correctly(): obj = LocalFourPoint(mat1, num_vn_dimensions=2) result = obj + mat2 expected = mat1 + mat2 - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_adds_local_four_point_and_local_interaction_correctly(): @@ -808,7 +808,7 @@ def test_adds_local_four_point_and_local_interaction_correctly(): obj2 = LocalInteraction(mat2) result = obj1 + obj2 expected = mat1 + mat2[..., None, None, None] - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def test_raises_error_for_unsupported_addition_type(): @@ -828,7 +828,7 @@ def test_adds_local_four_point_and_interaction_with_compressed_q_dimension(): result = obj1 + interaction assert isinstance(result, np.ndarray) expected = mat1[None, ...] + mat2[..., None, None, None] - assert np.allclose(result, expected, rtol=1e-4) + assert np.allclose(result, expected, atol=1e-4) assert result.shape[1:] == mat1.shape assert result.shape[0] == 5 @@ -842,7 +842,7 @@ def test_adds_local_four_point_and_interaction_with_decompressed_q_dimension(): result = obj1 + interaction assert isinstance(result, np.ndarray) expected = mat1[None, None, None, ...] + mat2[..., None, None, None] - assert np.allclose(result, expected, rtol=1e-4) + assert np.allclose(result, expected, atol=1e-4) assert result.shape[3:] == mat1.shape assert result.shape[:2] == mat2.shape[:2] @@ -879,7 +879,7 @@ def test_converts_to_full_niw_range_correctly_with_no_vn_dimensions(): result = obj.to_full_niw_range() expected = np.conj(np.flip(np.take(mat, np.arange(1, mat.shape[-1]), axis=-1), axis=-1)) expected = np.concatenate((expected, mat), axis=-1) - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) assert result.full_niw_range is True @@ -890,7 +890,7 @@ def test_converts_to_full_niw_range_correctly_with_one_vn_dimension(): result = obj.to_full_niw_range() expected = np.conj(np.flip(np.take(mat, np.arange(1, mat.shape[-2]), axis=-2), axis=(-2, -1))) expected = np.concatenate((expected, mat), axis=-2) - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) assert result.full_niw_range is True @@ -901,7 +901,7 @@ def test_converts_to_full_niw_range_correctly_with_two_vn_dimensions(): result = obj.to_full_niw_range() expected = np.conj(np.flip(np.take(mat, np.arange(1, mat.shape[-3]), axis=-3), axis=(-3, -2, -1))) expected = np.concatenate((expected, mat), axis=-3) - assert np.allclose(result.mat, expected, rtol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) assert result.full_niw_range is True @@ -910,7 +910,7 @@ def test_handles_already_full_niw_range_without_modification(): mat = np.random.rand(2, 2, 2, 2, 21, 4, 4) + 1j * np.random.rand(2, 2, 2, 2, 21, 4, 4) obj = LocalFourPoint(mat, num_vn_dimensions=2, full_niw_range=True) result = obj.to_full_niw_range() - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) assert result.full_niw_range is True @@ -1045,7 +1045,7 @@ def test_neg_dunder_calls_neg(): with patch.object(LocalFourPoint, "__neg__", wraps=obj.__neg__) as mock_neg: result = -obj mock_neg.assert_called() - assert np.allclose(result.mat, -obj.mat, rtol=1e-4) + assert np.allclose(result.mat, -obj.mat, atol=1e-4) @pytest.mark.parametrize("num_vn_dimensions", [0, 1, 2]) @@ -1057,7 +1057,7 @@ def test_creates_bosonic_dimension_when_not_present(num_vn_dimensions): result = obj.create_wn_dimension() assert result.num_wn_dimensions == 1 assert result.mat.shape == (2,) * 4 + (1,) + (4,) * num_vn_dimensions - assert np.allclose(result.mat, np.expand_dims(mat, axis=-(num_vn_dimensions + 1)), rtol=1e-4) + assert np.allclose(result.mat, np.expand_dims(mat, axis=-(num_vn_dimensions + 1)), atol=1e-4) @pytest.mark.parametrize("num_vn_dimensions", [0, 1, 2]) @@ -1079,7 +1079,7 @@ def test_removes_bosonic_dimension_correctly(num_vn_dimensions): result = obj.take_first_wn() assert result.num_wn_dimensions == 0 assert result.mat.shape == (2,) * 4 + (4,) * num_vn_dimensions - assert np.allclose(result.mat, np.take(mat, 0, axis=-(num_vn_dimensions + 1)), rtol=1e-4) + assert np.allclose(result.mat, np.take(mat, 0, axis=-(num_vn_dimensions + 1)), atol=1e-4) @pytest.mark.parametrize("num_vn_dimensions", [0, 1, 2]) @@ -1102,17 +1102,17 @@ def test_pads_with_u_correctly(niv_pad): result = obj.pad_with_u(u, niv_pad) assert result.mat.shape == (2, 2, 2, 2, 11, 2 * niv_pad, 2 * niv_pad) assert result.original_shape == result.mat.shape - assert np.allclose(result.mat[..., niv_pad - 4 : niv_pad + 4, niv_pad - 4 : niv_pad + 4], mat, rtol=1e-4) + assert np.allclose(result.mat[..., niv_pad - 4 : niv_pad + 4, niv_pad - 4 : niv_pad + 4], mat, atol=1e-4) - assert np.allclose(result.mat[..., : niv_pad - 4, :], u_mat[..., None, None, None], rtol=1e-4) - assert np.allclose(result.mat[..., : niv_pad - 4], u_mat[..., None, None, None], rtol=1e-4) - assert np.allclose(result.mat[..., niv_pad + 4 :, :], u_mat[..., None, None, None], rtol=1e-4) - assert np.allclose(result.mat[..., niv_pad + 4 :], u_mat[..., None, None, None], rtol=1e-4) + assert np.allclose(result.mat[..., : niv_pad - 4, :], u_mat[..., None, None, None], atol=1e-4) + assert np.allclose(result.mat[..., : niv_pad - 4], u_mat[..., None, None, None], atol=1e-4) + assert np.allclose(result.mat[..., niv_pad + 4 :, :], u_mat[..., None, None, None], atol=1e-4) + assert np.allclose(result.mat[..., niv_pad + 4 :], u_mat[..., None, None, None], atol=1e-4) - assert np.allclose(result.mat[..., niv_pad + 4 :, : niv_pad - 4], u_mat[..., None, None, None], rtol=1e-4) - assert np.allclose(result.mat[..., : niv_pad - 4, niv_pad + 4 :], u_mat[..., None, None, None], rtol=1e-4) - assert np.allclose(result.mat[..., : niv_pad - 4, : niv_pad - 4], u_mat[..., None, None, None], rtol=1e-4) - assert np.allclose(result.mat[..., niv_pad + 4 :, niv_pad + 4 :], u_mat[..., None, None, None], rtol=1e-4) + assert np.allclose(result.mat[..., niv_pad + 4 :, : niv_pad - 4], u_mat[..., None, None, None], atol=1e-4) + assert np.allclose(result.mat[..., : niv_pad - 4, niv_pad + 4 :], u_mat[..., None, None, None], atol=1e-4) + assert np.allclose(result.mat[..., : niv_pad - 4, : niv_pad - 4], u_mat[..., None, None, None], atol=1e-4) + assert np.allclose(result.mat[..., niv_pad + 4 :, niv_pad + 4 :], u_mat[..., None, None, None], atol=1e-4) @pytest.mark.parametrize("niv", [5, 10, 15]) @@ -1123,7 +1123,7 @@ def test_does_not_pad_when_niv_pad_is_less_or_equal(niv): obj = LocalFourPoint(mat, num_vn_dimensions=2) u = LocalInteraction(u_mat) result = obj.pad_with_u(u, niv) - assert np.allclose(result.mat, mat, rtol=1e-4) + assert np.allclose(result.mat, mat, atol=1e-4) assert result.mat.shape == mat.shape @@ -1145,7 +1145,7 @@ def test_pad_with_u_does_not_mutate_self(): u = LocalInteraction(np.random.rand(2, 2, 2, 2)) obj.pad_with_u(u, 8) assert obj.mat.shape == (2, 2, 2, 2, 5, 8, 8) - assert np.allclose(obj.mat, mat, rtol=1e-4) # source array left untouched + assert np.allclose(obj.mat, mat, atol=1e-4) # source array left untouched def test_symmetrize_orbitals_already_symmetrized(): @@ -1241,3 +1241,135 @@ def test_identity_like_matches_its_own_operand_shape(): ident = LocalFourPoint.identity_like(magn) result = ident + magn # must not raise on shape mismatch assert result.niv == magn.niv + + +def _compound_product_reference(mat1: np.ndarray, mat2: np.ndarray, notation: FrequencyNotation) -> np.ndarray: + """Compound-space matrix product of two full-index tensors [o,o,o,o,v,v'] in the given frequency notation + (ph: rows {1,2,v}, cols {4,3,v'}; pp: rows {1,3,v}, cols {4,2,v'}).""" + dim = mat1.shape[0] * mat1.shape[1] * mat1.shape[-1] + order = (0, 1, 4, 3, 2, 5) if notation == FrequencyNotation.PH else (0, 2, 4, 3, 1, 5) + compound_shape = tuple(np.array(mat1.shape)[list(order)]) + prod = np.transpose(mat1, order).reshape(dim, dim) @ np.transpose(mat2, order).reshape(dim, dim) + return np.transpose(prod.reshape(compound_shape), np.argsort(order)) + + +def _extend_to_vn_diagonal(mat: np.ndarray) -> np.ndarray: + """Extends a full-index tensor [o,o,o,o,v] (or [o,o,o,o] for none) to [o,o,o,o,v,v] with a diagonal (constant) + fermionic frequency structure.""" + n = mat.shape[-1] if mat.ndim == 5 else 1 + extended = np.zeros(mat.shape[:4] + (n, n) if mat.ndim == 5 else mat.shape + (1, 1), dtype=mat.dtype) + idx = np.arange(n) + extended[..., idx, idx] = mat if mat.ndim == 5 else mat[..., None] + return extended + + +@pytest.mark.parametrize("notation", [FrequencyNotation.PH, FrequencyNotation.PP]) +def test_matmul_propagates_frequency_notation_and_compound_pairing(notation): + """Matmul contracts in the compound space of the operands' notation and the result carries the frequency + notation of self, so pp results unravel with the acbd back-permute.""" + rng = np.random.default_rng(12) + o, niv = 2, 3 + shape = (o, o, o, o, 1, 2 * niv, 2 * niv) + mat1 = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + mat2 = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + x = LocalFourPoint(mat1.copy(), SpinChannel.DENS, 1, 2, True, True, notation) + y = LocalFourPoint(mat2.copy(), SpinChannel.DENS, 1, 2, True, True, notation) + z = x @ y + ref = _compound_product_reference( + mat1[:, :, :, :, 0].astype(np.complex64), mat2[:, :, :, :, 0].astype(np.complex64), notation + ) + assert z.frequency_notation == notation + assert np.allclose(z.mat[:, :, :, :, 0], ref, atol=1e-4) + + +@pytest.mark.parametrize("notation", [FrequencyNotation.PH, FrequencyNotation.PP]) +def test_matmul_mixed_vn_respects_frequency_notation(notation): + """The memory-saving 2vn @ 1vn matmul branch contracts with the notation's orbital pairing (the 1vn operand + acts nu-diagonally on the result's second frequency) and the result carries the frequency notation of self.""" + rng = np.random.default_rng(14) + o, niv = 2, 3 + shape2 = (o, o, o, o, 1, 2 * niv, 2 * niv) + shape1 = (o, o, o, o, 1, 2 * niv) + mat2v = rng.standard_normal(shape2) + 1j * rng.standard_normal(shape2) + mat1v = rng.standard_normal(shape1) + 1j * rng.standard_normal(shape1) + x = LocalFourPoint(mat2v.copy(), SpinChannel.DENS, 1, 2, True, True, notation) + y = LocalFourPoint(mat1v.copy(), SpinChannel.DENS, 1, 1, True, True, notation) + z = x @ y + ref = _compound_product_reference( + mat2v[:, :, :, :, 0].astype(np.complex64), + _extend_to_vn_diagonal(mat1v[:, :, :, :, 0].astype(np.complex64)), + notation, + ) + assert z.frequency_notation == notation + assert z.num_vn_dimensions == 2 + assert np.allclose(z.mat[:, :, :, :, 0], ref, atol=1e-4) + + +@pytest.mark.parametrize("notation", [FrequencyNotation.PH, FrequencyNotation.PP]) +def test_matmul_with_interaction_respects_frequency_notation(notation): + """4pt @ LocalInteraction (and the reversed order) contracts the frequency-constant bare interaction with the + notation's orbital pairing and keeps the frequency notation of the four-point operand.""" + rng = np.random.default_rng(15) + o, niv = 2, 3 + shape = (o, o, o, o, 1, 2 * niv, 2 * niv) + mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + umat = rng.standard_normal((o, o, o, o)) + 1j * rng.standard_normal((o, o, o, o)) + x = LocalFourPoint(mat.copy(), SpinChannel.DENS, 1, 2, True, True, notation) + u = LocalInteraction(umat.copy()) + u_ext = _extend_to_vn_diagonal(np.broadcast_to(umat[..., None], (o, o, o, o, 2 * niv)).astype(np.complex64)) + ref_left = _compound_product_reference(mat[:, :, :, :, 0].astype(np.complex64), u_ext, notation) + ref_right = _compound_product_reference(u_ext, mat[:, :, :, :, 0].astype(np.complex64), notation) + z_left = x @ u + z_right = u @ x + assert z_left.frequency_notation == notation + assert z_right.frequency_notation == notation + assert np.allclose(z_left.mat[:, :, :, :, 0], ref_left, atol=1e-4) + assert np.allclose(z_right.mat[:, :, :, :, 0], ref_right, atol=1e-4) + + +def test_matmul_rejects_mismatched_frequency_notations(): + """Multiplying two four-point objects living in different frequency notations raises.""" + mat = np.random.rand(2, 2, 2, 2, 1, 6, 6) + 1j * np.random.rand(2, 2, 2, 2, 1, 6, 6) + x = LocalFourPoint(mat.copy(), SpinChannel.DENS, 1, 2, True, True, FrequencyNotation.PP) + y = LocalFourPoint(mat.copy(), SpinChannel.DENS, 1, 2, True, True, FrequencyNotation.PH) + with pytest.raises(ValueError): + x @ y + + +def test_pow_pp_squares_in_pp_compound_space_without_explicit_identity(): + """obj ** 2 on a pp object squares in the pp compound space (rows {1,3,v}, cols {4,2,v'}) and keeps the PP + notation, with the matching identity derived internally via identity_like.""" + rng = np.random.default_rng(18) + o, niv = 2, 3 + shape = (o, o, o, o, 1, 2 * niv, 2 * niv) + mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + obj = LocalFourPoint(mat.copy(), SpinChannel.DENS, 1, 2, True, True, FrequencyNotation.PP) + result = obj**2 + mat64 = mat[:, :, :, :, 0].astype(np.complex64) + ref = _compound_product_reference(mat64, mat64, FrequencyNotation.PP) + assert result.frequency_notation == FrequencyNotation.PP + assert np.allclose(result.mat[:, :, :, :, 0], ref, atol=1e-4) + + +def test_pow_zero_returns_identity_in_own_frequency_notation(): + """obj ** 0 without an explicit identity returns the compound-space identity carrying the object's frequency + notation (the identity tensor delta_14 delta_23 delta_vv' is the same for both notations).""" + rng = np.random.default_rng(19) + o, niv = 2, 3 + shape = (o, o, o, o, 1, 2 * niv, 2 * niv) + mat = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + obj = LocalFourPoint(mat, SpinChannel.DENS, 1, 2, True, True, FrequencyNotation.PP) + result = obj**0 + expected = np.einsum("ad,bc,vp->abcdvp", np.eye(o), np.eye(o), np.eye(2 * niv)) + assert result.frequency_notation == FrequencyNotation.PP + assert np.allclose(result.mat[:, :, :, :, 0], expected, atol=1e-6) + + +def test_pow_rejects_identity_with_mismatched_frequency_notation(): + """pow with an explicitly passed identity in a different frequency notation raises instead of silently + returning or contracting a mislabeled object.""" + mat = np.random.rand(2, 2, 2, 2, 1, 6, 6) + 1j * np.random.rand(2, 2, 2, 2, 1, 6, 6) + obj = LocalFourPoint(mat, SpinChannel.DENS, 1, 2, True, True, FrequencyNotation.PP) + identity_ph = LocalFourPoint.identity(2, 0, 3, num_vn_dimensions=2, full_niw_range=True) + with pytest.raises(ValueError): + obj.pow(0, identity_ph) diff --git a/tests/test_local_n_point.py b/tests/test_local_n_point.py index b1ea3a53..cf9b5f51 100644 --- a/tests/test_local_n_point.py +++ b/tests/test_local_n_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import itertools @@ -245,15 +245,15 @@ def test_extends_correctly_with_one_fermionic_dimension(): result = obj.extend_vn_to_diagonal() assert result is obj assert result.mat.shape == (4, 4, 4, 4, 4) - assert np.allclose(result.mat[..., 0, 0], mat[..., 0], rtol=1e-2) - assert np.allclose(result.mat[..., 0, 1], 0, rtol=1e-2) - assert np.allclose(result.mat[..., 1, 0], 0, rtol=1e-2) - assert np.allclose(result.mat[..., 1, 1], mat[..., 1], rtol=1e-2) - assert np.allclose(result.mat[..., 2, 0], 0, rtol=1e-2) - assert np.allclose(result.mat[..., 0, 2], 0, rtol=1e-2) - assert np.allclose(result.mat[..., 2, 1], 0, rtol=1e-2) - assert np.allclose(result.mat[..., 1, 2], 0, rtol=1e-2) - assert np.allclose(result.mat[..., 2, 2], mat[..., 2], rtol=1e-2) + assert np.allclose(result.mat[..., 0, 0], mat[..., 0], atol=1e-2) + assert np.allclose(result.mat[..., 0, 1], 0, atol=1e-2) + assert np.allclose(result.mat[..., 1, 0], 0, atol=1e-2) + assert np.allclose(result.mat[..., 1, 1], mat[..., 1], atol=1e-2) + assert np.allclose(result.mat[..., 2, 0], 0, atol=1e-2) + assert np.allclose(result.mat[..., 0, 2], 0, atol=1e-2) + assert np.allclose(result.mat[..., 2, 1], 0, atol=1e-2) + assert np.allclose(result.mat[..., 1, 2], 0, atol=1e-2) + assert np.allclose(result.mat[..., 2, 2], mat[..., 2], atol=1e-2) def test_raises_error_when_taking_diagonal_with_no_fermionic_dimensions(): @@ -282,10 +282,10 @@ def test_compresses_correctly_with_two_fermionic_dimensions(): result = obj.take_vn_diagonal() assert result is obj assert result.mat.shape == (4, 4, 4, 4) - assert np.allclose(result.mat[..., 0], 1, rtol=1e-2) - assert np.allclose(result.mat[..., 1], 2, rtol=1e-2) - assert np.allclose(result.mat[..., 2], 3, rtol=1e-2) - assert np.allclose(result.mat[..., 3], 4, rtol=1e-2) + assert np.allclose(result.mat[..., 0], 1, atol=1e-2) + assert np.allclose(result.mat[..., 1], 2, atol=1e-2) + assert np.allclose(result.mat[..., 2], 3, atol=1e-2) + assert np.allclose(result.mat[..., 3], 4, atol=1e-2) def test_flips_matrix_along_valid_single_axis(): @@ -293,7 +293,7 @@ def test_flips_matrix_along_valid_single_axis(): mat = np.zeros((4, 4, 9, 10)) obj = LocalNPoint(mat, 2, 1, 1) result = obj.flip_frequency_axis(axis=(-1,)) - assert np.allclose(result.mat, np.flip(mat, axis=-1), rtol=1e-2) + assert np.allclose(result.mat, np.flip(mat, axis=-1), atol=1e-2) def test_flips_matrix_along_valid_multiple_axes(): @@ -301,7 +301,7 @@ def test_flips_matrix_along_valid_multiple_axes(): mat = np.zeros((4, 4, 9, 10)) obj = LocalNPoint(mat, 2, 1, 1) result = obj.flip_frequency_axis(axis=(-2, -1)) - assert np.allclose(result.mat, np.flip(mat, axis=(-2, -1)), rtol=1e-2) + assert np.allclose(result.mat, np.flip(mat, axis=(-2, -1)), atol=1e-2) def test_raises_error_when_flipping_with_no_frequency_dimensions(): @@ -328,7 +328,7 @@ def test_handles_single_axis_as_integer(): mat = np.zeros((4, 4, 9, 10)) obj = LocalNPoint(mat, 2, 1, 1) result = obj.flip_frequency_axis(axis=-1) - assert np.allclose(result.mat, np.flip(mat, axis=-1), rtol=1e-2) + assert np.allclose(result.mat, np.flip(mat, axis=-1), atol=1e-2) def test_aligns_frequency_dimensions_correctly_when_self_has_one_and_other_has_two_fermionic_dimensions(): @@ -401,7 +401,7 @@ def test_converts_to_half_bosonic_range_correctly(): result = obj.to_half_niw_range() assert result is obj assert result.mat.shape == (4, 4, 11, 20) - assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-2), rtol=1e-2) + assert np.allclose(result.mat, np.take(mat, np.arange(10, 21), axis=-2), atol=1e-2) def test_returns_self_when_already_in_half_bosonic_range(): diff --git a/tests/test_local_sde.py b/tests/test_local_sde.py index ae1d3a1a..0902f27d 100644 --- a/tests/test_local_sde.py +++ b/tests/test_local_sde.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import os diff --git a/tests/test_local_sde_end_to_end.py b/tests/test_local_sde_end_to_end.py index 1409d83e..c6ea67aa 100644 --- a/tests/test_local_sde_end_to_end.py +++ b/tests/test_local_sde_end_to_end.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import logging @@ -195,7 +195,7 @@ def test_calculates_local_sde_correctly(setup, niw_core, niv_core, niv_shell): u_loc = config.lattice.hamiltonian.get_local_u() - (gamma_d, gamma_m, chi_d, chi_m, vrg_d, vrg_m, f_d, f_m, gchi_d, gchi_m, sigma_loc) = ( + gamma_d, gamma_m, chi_d, chi_m, vrg_d, vrg_m, f_d, f_m, gchi_d, gchi_m, sigma_loc = ( local_sde.perform_local_schwinger_dyson(g_dmft, g2_dens, g2_magn, u_loc) ) diff --git a/tests/test_local_two_point.py b/tests/test_local_two_point.py index 645e5ba8..1d4a5331 100644 --- a/tests/test_local_two_point.py +++ b/tests/test_local_two_point.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems """ Tests for the shared two-point base classes :class:`LocalTwoPoint` (momentum-independent) and :class:`TwoPoint` diff --git a/tests/test_max_ent.py b/tests/test_max_ent.py index cd00fba2..a9cd30ed 100644 --- a/tests/test_max_ent.py +++ b/tests/test_max_ent.py @@ -1,9 +1,10 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems +import warnings from types import SimpleNamespace from unittest.mock import MagicMock @@ -230,8 +231,8 @@ def fn(comm, rank): assert np.allclose(spectrum[k], spectrum[flat_rep[k]], atol=1e-6) -def test_perform_maxent_giwk_failed_continuation_yields_zeros(tmp_path, monkeypatch): - """perform_maxent_giwk yields zeros when the analytic continuation raises.""" +def test_perform_maxent_giwk_failed_continuation_logs_kpoint_and_yields_zeros(tmp_path, monkeypatch): + """perform_maxent_giwk logs a per-k-point error (not a stack trace) and yields zeros when continuation raises.""" nk, n_bands, w_count = (4, 4, 1), 2, 6 _setup_maxent_config(tmp_path, nk, n_bands, w_count=w_count, seed=4) mat = _build_giwk_mat(nk, n_bands, niv=4, seed=19) @@ -249,6 +250,66 @@ def fn(comm, rank): _, results = run_parallel(1, fn) assert np.array_equal(results[0], np.zeros_like(results[0])) + # one log per failed (irreducible k-point, band), each naming the k-point and the A=0 fallback + fail_msgs = [ + call.args[0] + for call in config.logger.info.call_args_list + if "Failed to determine analytic continuation of k=" in call.args[0] + ] + assert len(fail_msgs) == config.lattice.k_grid.nk_irr * n_bands + assert all("setting A(k=" in m and "= 0.0" in m for m in fail_msgs) + + +def test_perform_maxent_giwk_reroutes_solver_prints_to_logger(tmp_path, monkeypatch): + """The vendored solver's stdout is captured and re-logged as 'ana_cont: ', not leaked to stdout.""" + nk, n_bands = (4, 4, 1), 1 + _setup_maxent_config(tmp_path, nk, n_bands, seed=6) + mat = _build_giwk_mat(nk, n_bands, niv=4, seed=23) + + class _PrintingProblem: + def __init__(self, *args, **kwargs): + pass + + def solve(self, *args, **kwargs): + print("Fermi fit failed.") + return (SimpleNamespace(A_opt=np.zeros(config.ana_cont.w_count)),) + + monkeypatch.setattr(mpi_utils, "MPI", FAKE_MPI) + monkeypatch.setattr(max_ent, "AnalyticContinuationProblem", _PrintingProblem) + + def fn(comm, rank): + return max_ent.perform_maxent_giwk(GreensFunction(mat.copy(), nk=config.lattice.nk), "TEST", comm) + + run_parallel(1, fn) + info_msgs = [call.args[0] for call in config.logger.info.call_args_list] + assert "ana_cont: Fermi fit failed." in info_msgs + + +def test_perform_maxent_giwk_runtime_warning_is_treated_as_failure(tmp_path, monkeypatch): + """A numpy/scipy RuntimeWarning during the continuation is escalated to a failure: A(k, w) = 0.""" + nk, n_bands = (4, 4, 1), 1 + _setup_maxent_config(tmp_path, nk, n_bands, seed=8) + mat = _build_giwk_mat(nk, n_bands, niv=4, seed=29) + + class _WarningProblem: + def __init__(self, *args, **kwargs): + pass + + def solve(self, *args, **kwargs): + warnings.warn("overflow encountered", RuntimeWarning) + return (SimpleNamespace(A_opt=np.ones(config.ana_cont.w_count)),) + + monkeypatch.setattr(mpi_utils, "MPI", FAKE_MPI) + monkeypatch.setattr(max_ent, "AnalyticContinuationProblem", _WarningProblem) + + def fn(comm, rank): + return max_ent.perform_maxent_giwk(GreensFunction(mat.copy(), nk=config.lattice.nk), "TEST", comm) + + _, results = run_parallel(1, fn) + assert np.array_equal(results[0], np.zeros_like(results[0])) + fail_msgs = [c.args[0] for c in config.logger.info.call_args_list if "Failed to determine" in c.args[0]] + assert len(fail_msgs) == config.lattice.k_grid.nk_irr * n_bands + def test_perform_maxent_giwk_single_band(tmp_path, patch_maxent_mpi): """perform_maxent_giwk reduces to the orbital-diagonal continuation for a single band.""" diff --git a/tests/test_memory_estimator.py b/tests/test_memory_estimator.py index 023adf80..315dd030 100644 --- a/tests/test_memory_estimator.py +++ b/tests/test_memory_estimator.py @@ -6,8 +6,14 @@ import pytest from dgamore.memory_estimator import ( + ARPACK_EXTRA_VECTORS, + CHI0Q_IFFTN_TRANSIENT_FACTOR, + CHIQ_AUX_INVERT_FACTOR, DTYPE_BYTES, + FQ_MATMUL_FACTOR, + LANCZOS_VERTEX_FACTOR, OVERHEAD_FACTOR, + SDE_FFT_KERNEL_FACTOR, BranchPeak, estimate_peaks, ) @@ -26,26 +32,29 @@ with_eliashberg=False, ) +TINY = dict( + n_bands=2, + nk_tot=80, + nk_irr=20, + niw_core=4, + niv_core=5, + niv_full=6, + niv_cut=15, + niv_pp=2, + n_ranks=4, + with_eliashberg=False, +) -def _estimate(**overrides): - return estimate_peaks(**{**BASE, **overrides}) +SCALE = DTYPE_BYTES * OVERHEAD_FACTOR def _peaks(**overrides): - return _estimate(**overrides)[1] - - -def _baseline(**overrides): - return _estimate(**overrides)[0] - - -# the node total used by the driver: every rank holds baseline + the distributed transient, plus one single-rank one -def _node_total(baseline, distributed, single, r): - return r * (baseline + distributed) + single + return estimate_peaks(**{**BASE, **overrides}) -def _off_node_total(bp: BranchPeak, baseline, r): - return _node_total(baseline, bp.off_distributed, bp.off_single, r) +# the node total used by the driver: every rank holds the branch baseline + the distributed transient, plus one single +def _off_node_total(bp: BranchPeak, r): + return r * (bp.baseline + bp.off_distributed) + bp.off_single def test_constants(): @@ -64,16 +73,11 @@ def test_keys_with_eliashberg(): assert set(_peaks(with_eliashberg=True)) == {"chi0q", "chiq_aux", "sde", "fq", "lanczos"} -def test_returns_baseline_and_branchpeaks(): - """estimate_peaks returns a positive baseline and a BranchPeak per branch.""" - baseline, peaks = _estimate(with_eliashberg=True) - assert baseline > 0 - assert all(isinstance(bp, BranchPeak) for bp in peaks.values()) - - -def test_every_branch_has_some_off_transient(): - """Every branch allocates a distributed or single-rank transient beyond the baseline.""" +def test_every_branch_has_positive_baseline_and_off_transient(): + """Every branch carries a positive per-branch baseline and some fast-path transient.""" for bp in _peaks(with_eliashberg=True).values(): + assert isinstance(bp, BranchPeak) + assert bp.baseline > 0 assert bp.off_distributed + bp.off_single > 0 @@ -95,16 +99,7 @@ def test_lanczos_fast_path_is_single_rank_only(): """The lanczos fast path is single-rank-only while its lean path is distributed.""" bp = _peaks(with_eliashberg=True)["lanczos"] assert bp.off_distributed == 0.0 and bp.off_single > 0.0 - assert bp.on_distributed > 0.0 and bp.on_single == 0.0 - - -def test_sde_and_fq_are_distributed_only(): - """The sde and fq branches are distributed-only with no single-rank transient.""" - peaks = _peaks(with_eliashberg=True) - for key in ("sde", "fq"): - bp = peaks[key] - assert bp.off_single == 0.0 and bp.on_single == 0.0 - assert bp.off_distributed > 0.0 + assert bp.on_distributed > 0.0 and bp.on_single > 0.0 # lean single: the root rank's full-BZ pp bubble def test_chi0q_single_rank_peak_independent_of_rank_count(): @@ -117,18 +112,27 @@ def test_chiq_aux_distributed_block_shrinks_with_more_ranks(): assert _peaks(n_ranks=16)["chiq_aux"].off_distributed < _peaks(n_ranks=2)["chiq_aux"].off_distributed -def test_lanczos_single_rank_independent_of_rank_count(): - """The lanczos single-rank peak is independent of the rank count.""" +def test_lanczos_single_rank_independent_of_rank_count_beyond_one(): + """The lanczos single-rank peak is rank-count-independent for multi-rank runs.""" few = _peaks(n_ranks=2, with_eliashberg=True)["lanczos"].off_single many = _peaks(n_ranks=8, with_eliashberg=True)["lanczos"].off_single assert few == pytest.approx(many) +def test_lanczos_single_rank_run_adds_waiting_channel_vertex(): + """A single-rank run solves the channels sequentially and holds the waiting channel's gathered irr-BZ vertex.""" + p = {**BASE, "with_eliashberg": True} + extra = SCALE * p["nk_irr"] * p["n_bands"] ** 4 * (2 * p["niv_pp"]) ** 2 + assert _peaks(n_ranks=1, with_eliashberg=True)["lanczos"].off_single == pytest.approx( + _peaks(n_ranks=2, with_eliashberg=True)["lanczos"].off_single + extra + ) + + def test_two_fermion_branches_dominate_node_total(): """The two-fermion branches (chiq_aux, fq) dominate the per-node memory total.""" - baseline, peaks = _estimate(with_eliashberg=True) + peaks = _peaks(with_eliashberg=True) r = BASE["n_ranks"] - totals = {k: _off_node_total(bp, baseline, r) for k, bp in peaks.items()} + totals = {k: _off_node_total(bp, r) for k, bp in peaks.items()} assert totals["chiq_aux"] > totals["chi0q"] assert totals["chiq_aux"] > totals["sde"] assert totals["fq"] > totals["sde"] @@ -137,87 +141,103 @@ def test_two_fermion_branches_dominate_node_total(): def test_node_total_monotonic_in_n_bands(): """The node total grows with the number of bands.""" r = BASE["n_ranks"] - small = _off_node_total(_peaks(n_bands=1)["chiq_aux"], _baseline(n_bands=1), r) - big = _off_node_total(_peaks(n_bands=2)["chiq_aux"], _baseline(n_bands=2), r) - assert big > small + assert _off_node_total(_peaks(n_bands=2)["chiq_aux"], r) > _off_node_total(_peaks(n_bands=1)["chiq_aux"], r) def test_overhead_scales_everything_linearly(): """The overhead factor scales the baseline and every branch linearly.""" - base1, peaks1 = estimate_peaks(**BASE, overhead=1.0) - base2, peaks2 = estimate_peaks(**BASE, overhead=2.0) - assert base2 == pytest.approx(2.0 * base1) + peaks1 = estimate_peaks(**BASE, overhead=1.0) + peaks2 = estimate_peaks(**BASE, overhead=2.0) + assert peaks2["chiq_aux"].baseline == pytest.approx(2.0 * peaks1["chiq_aux"].baseline) assert peaks2["chiq_aux"].off_distributed == pytest.approx(2.0 * peaks1["chiq_aux"].off_distributed) def test_fq_distributed_block_heavier_than_chiq_aux_block(): """The fq distributed block is heavier than chiq_aux (3 vs 2 two-fermion blocks per q).""" - from dgamore.memory_estimator import CHIQ_AUX_INVERT_FACTOR, FQ_MATMUL_FACTOR - assert FQ_MATMUL_FACTOR > CHIQ_AUX_INVERT_FACTOR peaks = _peaks(with_eliashberg=True) assert peaks["fq"].on_distributed > peaks["chiq_aux"].on_distributed -def test_baseline_is_giwk_plus_sigma_old_at_their_windows(): - """The baseline equals giwk_full plus sigma_old, both kept at the niv_cut window.""" - tiny = dict(BASE, n_bands=2, nk_tot=100, niw_core=5, niv_core=5, niv_full=7, niv_cut=22) - giwk = tiny["nk_tot"] * tiny["n_bands"] ** 2 * (2 * tiny["niv_cut"]) - sigma_old = tiny["nk_tot"] * tiny["n_bands"] ** 2 * (2 * tiny["niv_cut"]) - expected = DTYPE_BYTES * OVERHEAD_FACTOR * (giwk + sigma_old) - assert estimate_peaks(**tiny)[0] == pytest.approx(expected) +def test_bubble_baseline_is_giwk_plus_sigma_old_at_niv_cut(): + """The chi0q baseline equals giwk_full plus sigma_old, both at the niv_cut window.""" + expected = SCALE * 2 * (TINY["nk_tot"] * TINY["n_bands"] ** 2 * (2 * TINY["niv_cut"])) + assert estimate_peaks(**TINY)["chi0q"].baseline == pytest.approx(expected) + +def test_sde_section_baseline_uses_post_bubble_windows(): + """The chiq_aux/sde baseline holds giwk at the niv_core + niw_core window and sigma_old at the core box.""" + nk, nb = TINY["nk_tot"], TINY["n_bands"] + giwk = nk * nb**2 * 2 * (TINY["niv_core"] + TINY["niw_core"]) + sigma_old = nk * nb**2 * 2 * TINY["niv_core"] + peaks = estimate_peaks(**TINY) + assert peaks["chiq_aux"].baseline == pytest.approx(SCALE * (giwk + sigma_old)) + assert peaks["sde"].baseline == pytest.approx(peaks["chiq_aux"].baseline) + assert peaks["chiq_aux"].giwk_shareable == pytest.approx(SCALE * giwk) -def test_baseline_depends_on_niv_cut_not_niv_full(): - """The baseline tracks niv_cut and is independent of niv_full when niv_cut is fixed.""" - assert _baseline(niv_full=40) == pytest.approx(_baseline(niv_full=400)) - assert _baseline(niv_cut=80) != pytest.approx(_baseline(niv_cut=800)) +def test_giwk_shareable_is_the_giwk_part_of_each_sde_section_baseline(): + """giwk_shareable covers exactly the giwk_full part of the chi0q/chiq_aux/sde baselines.""" + peaks = _peaks(with_eliashberg=True) + assert peaks["chi0q"].giwk_shareable == pytest.approx(peaks["chi0q"].baseline / 2) + for key in ("chi0q", "chiq_aux", "sde"): + assert 0 < peaks[key].giwk_shareable < peaks[key].baseline -def test_chiq_aux_invert_factor_counts_construction_temporary(): - """CHIQ_AUX_INVERT_FACTOR is 2, counting the block-construction temporary kept live.""" - from dgamore.memory_estimator import CHIQ_AUX_INVERT_FACTOR - assert CHIQ_AUX_INVERT_FACTOR == 2 +def test_eliashberg_branches_are_not_giwk_shareable(): + """The fq/lanczos branches run on the private per-rank giwk_dga, so nothing is node-shared there.""" + peaks = _peaks(with_eliashberg=True) + for key in ("fq", "lanczos"): + assert peaks[key].giwk_shareable == 0.0 + assert peaks[key].baseline > 0.0 + + +def test_bubble_baseline_depends_on_niv_cut_not_niv_full(): + """The chi0q baseline tracks niv_cut and is independent of niv_full when niv_cut is fixed.""" + assert _peaks(niv_full=40)["chi0q"].baseline == pytest.approx(_peaks(niv_full=400)["chi0q"].baseline) + assert _peaks(niv_cut=80)["chi0q"].baseline != pytest.approx(_peaks(niv_cut=800)["chi0q"].baseline) def test_chiq_aux_off_block_is_two_rank_local_two_fermion_blocks(): """The chiq_aux off-distributed block equals two rank-local two-fermion blocks.""" - p = dict( - n_bands=2, nk_tot=80, nk_irr=20, niw_core=4, niv_core=5, niv_full=6, niv_cut=15, niv_pp=2, n_ranks=4, - with_eliashberg=False, - ) - _, peaks = estimate_peaks(**p) - nb, wp, vc = p["n_bands"], p["niw_core"] + 1, 2 * p["niv_core"] - qi = -(-p["nk_irr"] // p["n_ranks"]) + nb, wp, vc = TINY["n_bands"], TINY["niw_core"] + 1, 2 * TINY["niv_core"] + qi = -(-TINY["nk_irr"] // TINY["n_ranks"]) block = qi * nb**4 * wp * vc * vc - scale = DTYPE_BYTES * OVERHEAD_FACTOR - assert peaks["chiq_aux"].off_distributed == pytest.approx(scale * 2 * block) + assert estimate_peaks(**TINY)["chiq_aux"].off_distributed == pytest.approx(SCALE * CHIQ_AUX_INVERT_FACTOR * block) -def test_lanczos_lean_scales_with_full_bz(): - """The lanczos lean transient scales with the full BZ size (nk_tot), not the irreducible one.""" - small = _peaks(with_eliashberg=True, nk_tot=256)["lanczos"].on_distributed - big = _peaks(with_eliashberg=True, nk_tot=512)["lanczos"].on_distributed - assert big > small +def test_chi0q_fast_single_counts_buffer_ifftn_transient_and_g_copies(): + """The chi0q fast single-rank peak counts the multiply buffer, the ~2x ifftn transient and three G copies.""" + nb, wp, vf = TINY["n_bands"], TINY["niw_core"] + 1, 2 * TINY["niv_full"] + bubble_irr = TINY["nk_irr"] * nb**4 * wp * vf + fft_buffers = (1 + CHI0Q_IFFTN_TRANSIENT_FACTOR) * TINY["nk_tot"] * nb**4 * vf + gf_copies = 2 * TINY["nk_tot"] * nb**2 * (2 * (TINY["niv_full"] + TINY["niw_core"])) + g_center = TINY["nk_tot"] * nb**2 * vf + expected = SCALE * (bubble_irr + fft_buffers + gf_copies + g_center) + assert estimate_peaks(**TINY)["chi0q"].off_single == pytest.approx(expected) -def test_lanczos_lean_independent_of_irreducible_bz_size(): - """The lanczos lean transient is independent of the irreducible-BZ size.""" - a = _peaks(with_eliashberg=True, nk_irr=10)["lanczos"].on_distributed - b = _peaks(with_eliashberg=True, nk_irr=60)["lanczos"].on_distributed - assert a == pytest.approx(b) +def test_sde_holds_two_kernels_plus_irr_kernel_and_g_copy(): + """The (single, flag-less) sde FFT path counts the pass kernel blocks, the retained irr-BZ kernel and the + R-space G copy.""" + nb, wp, vc = TINY["n_bands"], TINY["niw_core"] + 1, 2 * TINY["niv_core"] + qt = -(-TINY["nk_tot"] // TINY["n_ranks"]) + qi = -(-TINY["nk_irr"] // TINY["n_ranks"]) + g_sde = TINY["nk_tot"] * nb**2 * 2 * (TINY["niv_core"] + TINY["niw_core"]) + expected = SCALE * (SDE_FFT_KERNEL_FACTOR * qt * nb**4 * wp * vc + qi * nb**4 * wp * vc + g_sde) + assert estimate_peaks(**TINY)["sde"].off_distributed == pytest.approx(expected) -def test_sde_lean_includes_green_function_copy(): - """The sde lean transient includes a full Green's-function copy at the niv_cut window.""" - a = _peaks(niv_cut=80)["sde"].on_distributed - b = _peaks(niv_cut=160)["sde"].on_distributed - assert b > a +def test_sde_off_and_on_slots_are_identical(): + """The sde step has no save_memory switch (the q-loop variant is unused), so both path slots carry the same + two-pass FFT estimate.""" + bp = _peaks()["sde"] + assert bp.on_distributed == pytest.approx(bp.off_distributed) + assert bp.on_single == pytest.approx(bp.off_single) -def test_fq_lean_includes_rank_local_accumulator(): - """The fq lean transient grows with the per-rank q-count via the rank-local accumulator.""" +def test_fq_lean_includes_rank_local_accumulator_and_loads(): + """The fq lean transient grows with the per-rank q-count via the accumulator and the three 1-fermion loads.""" few_ranks = _peaks(with_eliashberg=True, n_ranks=2)["fq"].on_distributed many_ranks = _peaks(with_eliashberg=True, n_ranks=8)["fq"].on_distributed assert few_ranks > many_ranks @@ -230,6 +250,18 @@ def test_fq_lean_accumulator_larger_when_save_fq(): assert big > small +def test_fq_save_fq_gathers_whole_irr_vertex_on_one_rank(): + """save_fq gathers the whole irreducible-BZ two-fermion vertex on one rank in both fq paths.""" + p = {**TINY, "with_eliashberg": True} + nb, wp, vc = p["n_bands"], p["niw_core"] + 1, 2 * p["niv_core"] + expected = SCALE * p["nk_irr"] * nb**4 * wp * vc * vc + peaks = estimate_peaks(**{**p, "save_fq": True}) + assert peaks["fq"].off_single == pytest.approx(expected) + assert peaks["fq"].on_single == pytest.approx(expected) + no_save = estimate_peaks(**{**p, "save_fq": False}) + assert no_save["fq"].off_single == 0.0 and no_save["fq"].on_single == 0.0 + + def test_fq_cheap_construction_shrinks_per_q_block(): """construct_fq_cheap cuts the inputs to niv_pp, shrinking every per-q two-fermion block.""" normal = _peaks(with_eliashberg=True, construct_fq_cheap=False)["fq"].on_distributed @@ -237,18 +269,53 @@ def test_fq_cheap_construction_shrinks_per_q_block(): assert cheap < normal -def test_chi0q_fast_single_counts_two_full_grid_buffers(): - """The chi0q fast single-rank peak counts both the multiply buffer and the ifftn output.""" - p = dict( - n_bands=2, nk_tot=80, nk_irr=20, niw_core=4, niv_core=5, niv_full=6, niv_cut=15, niv_pp=2, n_ranks=4, - with_eliashberg=False, - ) - _, peaks = estimate_peaks(**p) - nb, wp, vf = p["n_bands"], p["niw_core"] + 1, 2 * p["niv_full"] - bubble_irr = p["nk_irr"] * nb**4 * wp * vf - chi_r_v_buffer = p["nk_tot"] * nb**4 * vf # preallocated multiply target - ifftn_output = p["nk_tot"] * nb**4 * vf # xp.fft.ifftn returns a second full-grid buffer of the same size - gf_copies = 2 * p["nk_tot"] * nb**2 * (2 * (p["niv_full"] + p["niw_core"])) - scale = DTYPE_BYTES * OVERHEAD_FACTOR - expected = scale * (bubble_irr + chi_r_v_buffer + ifftn_output + gf_copies) - assert peaks["chi0q"].off_single == pytest.approx(expected) +def test_lanczos_fast_counts_three_vertices_bubble_and_arpack_basis(): + """The lanczos fast single-rank peak holds 3 full-BZ vertices, the pp bubble and the ARPACK workspace.""" + p = {**TINY, "with_eliashberg": True, "n_ranks": 4} + nb, vpp = p["n_bands"], 2 * p["niv_pp"] + vertex = p["nk_tot"] * nb**4 * vpp * vpp + chi0 = p["nk_tot"] * nb**4 * vpp + arpack = (max(2 * 1 + 1, 20) + ARPACK_EXTRA_VECTORS) * p["nk_tot"] * nb**2 * vpp + expected = SCALE * (LANCZOS_VERTEX_FACTOR * vertex + chi0 + arpack) + assert estimate_peaks(**p)["lanczos"].off_single == pytest.approx(expected) + + +def test_lanczos_lean_scales_with_full_bz(): + """The lanczos lean transient scales with the full BZ size (nk_tot), not the irreducible one.""" + small = _peaks(with_eliashberg=True, nk_tot=256)["lanczos"].on_distributed + big = _peaks(with_eliashberg=True, nk_tot=512)["lanczos"].on_distributed + assert big > small + + +def test_lanczos_lean_independent_of_irreducible_bz_size(): + """The lanczos lean transient is independent of the irreducible-BZ size.""" + a = _peaks(with_eliashberg=True, nk_irr=10)["lanczos"].on_distributed + b = _peaks(with_eliashberg=True, nk_irr=60)["lanczos"].on_distributed + assert a == pytest.approx(b) + + +def test_lanczos_lean_vertex_share_saturates_beyond_v_task_count(): + """The lean vertex share is bounded by the v-axis task count (2*niv_pp tasks), not the rank count.""" + at_tasks = _peaks(with_eliashberg=True, n_ranks=2 * BASE["niv_pp"])["lanczos"].on_distributed + beyond = _peaks(with_eliashberg=True, n_ranks=8 * BASE["niv_pp"])["lanczos"].on_distributed + assert beyond == pytest.approx(at_tasks) + + +def test_lanczos_arpack_workspace_grows_with_n_eig(): + """Requesting more eigenpairs than the default ncv=20 basis grows the per-rank ARPACK workspace.""" + default = _peaks(with_eliashberg=True, n_eig=1)["lanczos"] + many = _peaks(with_eliashberg=True, n_eig=30)["lanczos"] + assert many.off_single > default.off_single + assert many.on_distributed > default.on_distributed + + +def test_save_pairing_vertex_sets_the_lean_single_rank_gather(): + """save_pairing_vertex gathers both irr-BZ pp vertices on one rank, dominating the lean single-rank peak.""" + p = {**TINY, "with_eliashberg": True} + nb, vpp = p["n_bands"], 2 * p["niv_pp"] + gather = SCALE * 2 * p["nk_irr"] * nb**4 * vpp * vpp + chi0 = SCALE * p["nk_tot"] * nb**4 * vpp + with_save = estimate_peaks(**{**p, "save_pairing_vertex": True})["lanczos"] + without = estimate_peaks(**{**p, "save_pairing_vertex": False})["lanczos"] + assert with_save.on_single == pytest.approx(max(chi0, gather)) + assert without.on_single == pytest.approx(chi0) diff --git a/tests/test_mixing.py b/tests/test_mixing.py index 4c8b5664..2c3e7765 100644 --- a/tests/test_mixing.py +++ b/tests/test_mixing.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems from copy import deepcopy @@ -104,7 +104,7 @@ def test_linear_mixing_basic(): with patch_config(strategy="linear", mixing=0.5): result = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter=1) - np.testing.assert_allclose(result.mat, 1.0, atol=1e-5) + assert np.allclose(result.mat, 1.0, atol=1e-5) def test_linear_mixing_alpha_zero(): @@ -116,7 +116,7 @@ def test_linear_mixing_alpha_zero(): with patch_config(strategy="linear", mixing=0.0): result = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter=1) - np.testing.assert_allclose(result.mat, 1.0, atol=1e-5) + assert np.allclose(result.mat, 1.0, atol=1e-5) def test_linear_mixing_alpha_one(): @@ -128,7 +128,7 @@ def test_linear_mixing_alpha_one(): with patch_config(strategy="linear", mixing=1.0): result = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter=1) - np.testing.assert_allclose(result.mat, 5.0, atol=1e-5) + assert np.allclose(result.mat, 5.0, atol=1e-5) def test_linear_mixing_complex(): @@ -140,7 +140,7 @@ def test_linear_mixing_complex(): with patch_config(strategy="linear", mixing=0.5): result = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter=1) - np.testing.assert_allclose(result.mat, 1.0 + 1.0j, atol=1e-5) + assert np.allclose(result.mat, 1.0 + 1.0j, atol=1e-5) def test_linear_mixing_returns_self_energy_instance(): @@ -164,7 +164,7 @@ def test_pulay_falls_back_to_linear_when_iter_too_small(): with patch_config(strategy="pulay", mixing=0.5, n_hist=5): result = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter=3) - np.testing.assert_allclose(result.mat, 1.0, atol=1e-5) + assert np.allclose(result.mat, 1.0, atol=1e-5) def test_pulay_returns_self_energy_instance(): @@ -190,7 +190,7 @@ def test_pulay_converged_fixed_point(): result = run_pulay(sigma_new, sigma_old, sigma_dmft, file_sigmas) niv_dmft = sigma_new.mat.shape[-1] // 2 - np.testing.assert_allclose( + assert np.allclose( result.mat[..., niv_dmft - NIV_CORE : niv_dmft + NIV_CORE], np.full_like(result.mat[..., niv_dmft - NIV_CORE : niv_dmft + NIV_CORE], value), atol=1e-4, @@ -219,7 +219,7 @@ def test_pulay_does_not_mutate_sigma_old(): original_mat = sigma_old.mat.copy() run_pulay(sigma_new, sigma_old, sigma_dmft, file_sigmas) - np.testing.assert_array_equal(sigma_old.mat, original_mat) + assert np.array_equal(sigma_old.mat, original_mat) def test_pulay_tails_come_from_sigma_new(): @@ -232,8 +232,8 @@ def test_pulay_tails_come_from_sigma_new(): result = run_pulay(sigma_new, sigma_old, sigma_dmft, file_sigmas) niv_dmft = sigma_new.mat.shape[-1] // 2 - np.testing.assert_allclose(result.mat[..., : niv_dmft - NIV_CORE], 2.0, atol=1e-5) - np.testing.assert_allclose(result.mat[..., niv_dmft + NIV_CORE :], 2.0, atol=1e-5) + assert np.allclose(result.mat[..., : niv_dmft - NIV_CORE], 2.0, atol=1e-5) + assert np.allclose(result.mat[..., niv_dmft + NIV_CORE :], 2.0, atol=1e-5) def test_pulay_result_shape_matches_sigma_new(): @@ -271,7 +271,7 @@ def test_anderson_falls_back_to_linear_when_iter_too_small(): with patch_config(strategy="anderson", mixing=0.5, n_hist=5): result = apply_mixing_strategy(sigma_new, sigma_old, sigma_dmft, current_iter=3) - np.testing.assert_allclose(result.mat, 1.0, atol=1e-5) + assert np.allclose(result.mat, 1.0, atol=1e-5) def test_anderson_returns_self_energy_instance(): @@ -297,7 +297,7 @@ def test_anderson_converged_fixed_point(): result = run_anderson(sigma_new, sigma_old, sigma_dmft, file_sigmas) niv_dmft = sigma_new.mat.shape[-1] // 2 - np.testing.assert_allclose( + assert np.allclose( result.mat[..., niv_dmft - NIV_CORE : niv_dmft + NIV_CORE], np.full_like(result.mat[..., niv_dmft - NIV_CORE : niv_dmft + NIV_CORE], value), atol=1e-4, @@ -326,7 +326,7 @@ def test_anderson_does_not_mutate_sigma_old(): original_mat = sigma_old.mat.copy() run_anderson(sigma_new, sigma_old, sigma_dmft, file_sigmas) - np.testing.assert_array_equal(sigma_old.mat, original_mat) + assert np.array_equal(sigma_old.mat, original_mat) def test_anderson_tails_come_from_sigma_new(): @@ -339,8 +339,8 @@ def test_anderson_tails_come_from_sigma_new(): result = run_anderson(sigma_new, sigma_old, sigma_dmft, file_sigmas) niv_dmft = sigma_new.mat.shape[-1] // 2 - np.testing.assert_allclose(result.mat[..., : niv_dmft - NIV_CORE], 2.0, atol=1e-5) - np.testing.assert_allclose(result.mat[..., niv_dmft + NIV_CORE :], 2.0, atol=1e-5) + assert np.allclose(result.mat[..., : niv_dmft - NIV_CORE], 2.0, atol=1e-5) + assert np.allclose(result.mat[..., niv_dmft + NIV_CORE :], 2.0, atol=1e-5) def test_anderson_result_shape_matches_sigma_new(): diff --git a/tests/test_mpi_utils.py b/tests/test_mpi_utils.py index 24193a97..0632c30d 100644 --- a/tests/test_mpi_utils.py +++ b/tests/test_mpi_utils.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import os @@ -1175,3 +1175,97 @@ def test_isend_rows_rejects_non_contiguous(): arr = (np.arange(6 * 2).reshape(6, 2) + 1j).astype(np.complex128).T # F-contiguous view with pytest.raises(ValueError): mu._isend_rows(comm1(), arr, dest=0) + + +def test_build_node_shared_array_single_rank_returns_private_array(): + """A single-rank node short-circuits to a private array (no window) and still computes once.""" + calls = [] + + def fn(comm, rank): + node_comm = comm.Split_type(MPI.COMM_TYPE_SHARED) + + def compute(): + calls.append(rank) + return np.full((2, 3), 5.0, dtype=np.complex64) + + arr, win = mu.build_node_shared_array(node_comm, compute) + return np.array(arr), win + + _, res = run_parallel(1, fn) + assert calls == [0] + assert np.array_equal(res[0][0], np.full((2, 3), 5.0, dtype=np.complex64)) + assert res[0][1] is None + + +def test_build_node_shared_array_computes_once_and_shares_within_node(): + """On one node the root computes once and every rank maps the same populated buffer through a window.""" + calls = [] + + def fn(comm, rank): + node_comm = comm.Split_type(MPI.COMM_TYPE_SHARED) + + def compute(): + calls.append(rank) + return np.full((2, 3), 7.0, dtype=np.complex64) + + arr, win = mu.build_node_shared_array(node_comm, compute) + seen = np.array(arr) + node_comm.Barrier() + if win is not None: + win.Free() + node_comm.Free() + return seen, win is not None + + _, res = run_parallel(4, fn, hostnames=["h", "h", "h", "h"]) + assert calls == [0] + for seen, has_win in res: + assert has_win + assert np.array_equal(seen, np.full((2, 3), 7.0, dtype=np.complex64)) + + +def test_build_node_shared_array_isolates_between_nodes(): + """Two nodes each compute on their own root; ranks see only their own node's buffer.""" + calls = [] + + def fn(comm, rank): + node_comm = comm.Split_type(MPI.COMM_TYPE_SHARED) + + def compute(): + calls.append(rank) + return np.full((3,), float(rank + 1), dtype=np.complex64) + + arr, win = mu.build_node_shared_array(node_comm, compute) + seen = np.array(arr) + node_comm.Barrier() + if win is not None: + win.Free() + node_comm.Free() + return seen + + _, res = run_parallel(4, fn, hostnames=["n0", "n0", "n1", "n1"]) + assert sorted(calls) == [0, 2] # one compute per node root (global ranks 0 and 2) + assert np.array_equal(res[0], np.full((3,), 1.0, dtype=np.complex64)) + assert np.array_equal(res[1], np.full((3,), 1.0, dtype=np.complex64)) + assert np.array_equal(res[2], np.full((3,), 3.0, dtype=np.complex64)) + assert np.array_equal(res[3], np.full((3,), 3.0, dtype=np.complex64)) + + +def test_build_node_shared_array_view_is_live_shared_memory(): + """A write by the root after construction is visible to the other node ranks (a true shared view).""" + + def fn(comm, rank): + node_comm = comm.Split_type(MPI.COMM_TYPE_SHARED) + arr, win = mu.build_node_shared_array(node_comm, lambda: np.zeros((4,), dtype=np.complex64)) + if node_comm.Get_rank() == 0: + arr[:] = np.arange(4) # mutate the shared buffer after it was built + node_comm.Barrier() + seen = np.array(arr) + node_comm.Barrier() + if win is not None: + win.Free() + node_comm.Free() + return seen + + _, res = run_parallel(2, fn, hostnames=["h", "h"]) + for seen in res: + assert np.array_equal(seen, np.arange(4).astype(np.complex64)) diff --git a/tests/test_n_point_base.py b/tests/test_n_point_base.py index 7b689d71..47196fa2 100644 --- a/tests/test_n_point_base.py +++ b/tests/test_n_point_base.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import os @@ -828,6 +828,7 @@ def test_skip_on_non_posix_or_no_proc(monkeypatch): def test_loads_libc_and_calls_malloc_trim(monkeypatch): """malloc_trim loads libc and calls malloc_trim when available.""" + # simulate posix with /proc and a working ctypes.CDLL returning a libc with malloc_trim class FakeLib: def __init__(self): @@ -855,6 +856,7 @@ def malloc_trim(self, arg): def test_ctypes_cdll_failure_sets_unavailable(monkeypatch): """A ctypes CDLL failure marks malloc_trim unavailable.""" + # simulate posix with /proc but CDLL raises -> should mark unavailable and not raise def failing_cdll(name): raise OSError("no libc") @@ -874,6 +876,7 @@ def failing_cdll(name): def test_malloc_trim_exception_is_suppressed(monkeypatch): """An exception from malloc_trim is suppressed.""" + # simulate libc present but malloc_trim itself raises -> should be suppressed (no exception) class BadLib: def malloc_trim(self, arg): @@ -1316,7 +1319,7 @@ def test_map_to_full_bz_auto_2idx_reconstructs_H_exactly(): def test_map_to_full_bz_auto_2idx_reconstructs_H_for_multiorbital_case(): - """Same as above but with multiple orbitals — exercises the orbital einsum path.""" + """Same as above but with multiple orbitals - exercises the orbital einsum path.""" grid, H = _build_auto_kgrid(nx=4, ny=4, nz=4, nb=2) nb = 2 H_flat = H.reshape(-1, nb, nb) @@ -1483,7 +1486,7 @@ def test_map_to_full_bz_auto_1x1x1_trivial_grid_is_identity(): def test_map_to_full_bz_auto_preserves_dtype(): - """The output matrix has the same dtype as the input (the function does not silently cast within the auto branch — the cast to complex64 happens elsewhere in ``IHaveMat.mat = value``).""" + """The output matrix has the same dtype as the input (the function does not silently cast within the auto branch - the cast to complex64 happens elsewhere in ``IHaveMat.mat = value``).""" grid, H = _build_auto_kgrid(nx=4, ny=4, nz=4, nb=1) H_ibz_64 = H.reshape(-1, 1, 1)[grid.irrk_ind].astype(np.complex64).copy() obj = _DoublePrecisionNonLocal(mat=H_ibz_64, nq=(4, 4, 4), has_compressed_q_dimension=True) diff --git a/tests/test_nonlocal_sde.py b/tests/test_nonlocal_sde.py index 8eeaecdb..400454e6 100644 --- a/tests/test_nonlocal_sde.py +++ b/tests/test_nonlocal_sde.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import itertools @@ -12,11 +12,22 @@ import dgamore.config as config import dgamore.nonlocal_sde as nonlocal_sde +from dgamore.greens_function import GreensFunction from dgamore.hamiltonian import Hamiltonian from dgamore.interaction import Interaction from dgamore.local_sde import get_local_hartree_fock from dgamore.n_point_base import SpinChannel -from dgamore.nonlocal_sde import _init_mu_history, get_hartree_fock, perform_ornstein_zernike_fit +from dgamore.nonlocal_sde import ( + _build_giwk_full, + _cut_and_reshare_giwk, + _free_shared_window, + _init_mu_history, + _release_shared_giwk, + get_hartree_fock, + perform_ornstein_zernike_fit, +) +from dgamore.self_energy import SelfEnergy +from tests.conftest import create_comm_mock LOCAL_SDE_DATA = f"{os.path.dirname(os.path.abspath(__file__))}/test_data/local_sde" @@ -91,6 +102,10 @@ def __init__(self, mat: np.ndarray): """Stores the orbital-resolved matrix that the reduction chain returns unchanged.""" self._mat = mat + def copy(self): + """Identity copy; the reduction chain is non-mutating so a fresh wrapper suffices.""" + return _ConstantChi(self._mat) + def map_to_full_bz(self, grid): """Identity unfolding to the full BZ.""" return self @@ -134,3 +149,156 @@ def test_ornstein_zernike_fit_logs_no_warning_when_all_converge(monkeypatch): perform_ornstein_zernike_fit(_ConstantChi(np.ones((2, 2, 1, 2, 2, 2, 2), dtype=np.complex64))) logger.warning.assert_not_called() + + +def _tiny_sigma_and_ek(nb=1, niv=4): + """Builds a minimal single-k self-energy and dispersion for the Dyson build.""" + sigma = SelfEnergy(np.zeros((1, 1, 1, nb, nb, 2 * niv), dtype=np.complex64), calc_smom=False, beta=10.0) + ek = np.zeros((1, 1, 1, nb, nb), dtype=np.complex64) + return sigma, ek + + +def test_build_giwk_full_disabled_matches_direct_dyson_and_skips_split(): + """With node-sharing off, _build_giwk_full is the plain Dyson build and never touches the communicator.""" + config.memory.use_shared_memory_giwk = False + sigma, ek = _tiny_sigma_and_ek() + comm = create_comm_mock() + + giwk, win, node_comm = _build_giwk_full(comm, sigma, 0.3, ek, 10.0) + + assert win is None and node_comm is None + assert comm.Split_type.called is False + assert np.allclose(giwk.mat, GreensFunction.get_g_full(sigma, 0.3, ek, 10.0).mat, atol=1e-6) + + +def test_build_giwk_full_shared_single_rank_matches_direct_dyson(): + """With node-sharing on but a single-rank node, the giwk is bit-parity with the direct Dyson build (no window).""" + config.memory.use_shared_memory_giwk = True + sigma, ek = _tiny_sigma_and_ek() + comm = create_comm_mock() + + giwk, win, node_comm = _build_giwk_full(comm, sigma, 0.3, ek, 10.0) + + assert win is None # a single-rank node needs no shared window + assert np.array_equal(giwk.mat, GreensFunction.get_g_full(sigma, 0.3, ek, 10.0).mat) + _release_shared_giwk(win, node_comm) # must not raise on the single-rank / mock path + + +def test_release_shared_giwk_without_sharing_is_noop(): + """Releasing a non-shared giwk (both handles None) is a safe no-op.""" + _release_shared_giwk(None, None) + + +def test_release_shared_giwk_frees_window_and_communicator(): + """With a window allocated, _release barriers (so no rank still reads), frees the window, then the node comm.""" + win, node_comm = mock.Mock(), mock.Mock() + _release_shared_giwk(win, node_comm) + node_comm.Barrier.assert_called_once() + win.Free.assert_called_once() + node_comm.Free.assert_called_once() + + +def test_free_shared_window_frees_window_but_keeps_communicator(): + """_free_shared_window barriers and frees the window but leaves the node communicator alive (reused for the cut).""" + win, node_comm = mock.Mock(), mock.Mock() + _free_shared_window(win, node_comm) + node_comm.Barrier.assert_called_once() + win.Free.assert_called_once() + node_comm.Free.assert_not_called() + + +def test_cut_and_reshare_giwk_without_sharing_is_a_plain_cut(): + """Without a node communicator, _cut_and_reshare_giwk is a plain per-rank cut and allocates no window.""" + sigma, ek = _tiny_sigma_and_ek(niv=8) + giwk = GreensFunction.get_g_full(sigma, 0.3, ek, 10.0) + cut, win = _cut_and_reshare_giwk(giwk, None, None, 4) + assert win is None + assert cut.niv == 4 + assert np.array_equal(cut.mat, GreensFunction.get_g_full(sigma, 0.3, ek, 10.0).cut_niv(4).mat) + + +def test_cut_and_reshare_giwk_shared_single_rank_matches_plain_cut(): + """With a single-rank node communicator the cut giwk matches the plain cut and needs no window.""" + sigma, ek = _tiny_sigma_and_ek(niv=8) + giwk = GreensFunction.get_g_full(sigma, 0.3, ek, 10.0) + cut, win = _cut_and_reshare_giwk(giwk, None, create_comm_mock(), 4) + assert win is None + assert cut.niv == 4 + assert np.array_equal(cut.mat, GreensFunction.get_g_full(sigma, 0.3, ek, 10.0).cut_niv(4).mat) + + +def test_vrg_right_is_first_frequency_summed_three_leg_vertex(): + """The right-sided three-leg vertex must equal its definition gamma-tilde^{qv}_{1234} = beta sum_{ab} sum_{v_1} + chi*^{q v_1 v}_{12ab} (chi^{qv}_{0;ba34})^{-1}, which the dcba orbital permutation of the last-frequency-summed + chi* provides for a time-reversal-symmetric chi* (chi*^{q v v'}_{1234} = chi*^{q v' v}_{4321}); the left vertex + gamma^{qv}_{1234} = beta sum_{ab} sum_{v'} (chi^{qv}_{0;12ab})^{-1} chi*^{q v v'}_{ba34} is locked alongside.""" + from dgamore.four_point import FourPoint + from dgamore.n_point_base import SpinChannel + + o, nqi, nw, n2, beta = 2, 3, 3, 4, 12.5 + config.sys.beta = beta + rng = np.random.default_rng(11) + shape = (nqi, o, o, o, o, nw, n2, n2) + chi_star = rng.standard_normal(shape) + 1j * rng.standard_normal(shape) + chi_star = 0.5 * (chi_star + np.transpose(chi_star, (0, 4, 3, 2, 1, 5, 7, 6))) + chi0_inv = rng.standard_normal(shape[:-1]) + 1j * rng.standard_normal(shape[:-1]) + + sum_last = FourPoint(chi_star.sum(axis=-1) / beta, SpinChannel.DENS, (nqi, 1, 1), 1, 1, False, True, True) + chi0_inv_fp = FourPoint(chi0_inv.copy(), SpinChannel.NONE, (nqi, 1, 1), 1, 1, False, True, True) + + vrg_left = nonlocal_sde.create_vrg_r_q(sum_last.copy(), chi0_inv_fp) + ref_left = np.einsum("qabefwv,qfecdwv->qabcdwv", chi0_inv, chi_star.sum(axis=-1), optimize=True) + assert np.allclose(vrg_left.mat, ref_left, atol=1e-10) + + vrg_right = nonlocal_sde.create_vrg_r_q_right(sum_last, chi0_inv_fp) + ref_right = np.einsum("qabefwv,qfecdwv->qabcdwv", chi_star.sum(axis=-2), chi0_inv, optimize=True) + assert np.allclose(vrg_right.mat, ref_right, atol=1e-10) + + +def test_unused_qloop_sigma_variants_agree(): + """The unused q-loop self-energy contraction variants - the plain reference, the Fortran-buffered CPU one, the + GPU one (run through a numpy-backed cupy stub) and the auto dispatcher (falling back to the CPU) - produce + matching self-energies on synthetic full-BZ kernel and Green's-function data, locking the kept functions.""" + import types + + import dgamore.brillouin_zone as bz + from dgamore.four_point import FourPoint + + nk, o, niw, niv = (4, 4, 1), 2, 3, 4 + config.lattice.nk = nk + config.lattice.k_grid = bz.KGrid(nk, symmetries=[]) + config.lattice.q_grid = config.lattice.k_grid + config.box.niw_core = niw + config.box.niv_core = niv + config.sys.n_bands = o + config.sys.beta = 12.5 + config.logger = mock.MagicMock() + + rng = np.random.default_rng(7) + niv_g = niv + niw + 2 + g_shape = (*nk, o, o, 2 * niv_g) + k_shape = (int(np.prod(nk)), o, o, o, o, niw + 1, 2 * niv) + g_mat = (rng.standard_normal(g_shape) + 1j * rng.standard_normal(g_shape)).astype(np.complex64) + kernel_mat = (rng.standard_normal(k_shape) + 1j * rng.standard_normal(k_shape)).astype(np.complex64) + giwk = GreensFunction(g_mat, calc_filling=False, nk=nk, beta=config.sys.beta) + q_list = config.lattice.q_grid.get_q_list() + + def make_kernel(): + return FourPoint( + kernel_mat.copy(), SpinChannel.NONE, nk, 1, 1, full_niw_range=False, has_compressed_q_dimension=True + ) + + sigma_ref = nonlocal_sde.calculate_sigma_from_kernel(make_kernel(), giwk, q_list) + sigma_cpu = nonlocal_sde.calculate_sigma_from_kernel_cpu(make_kernel(), giwk, q_list) + + cupy_stub = types.ModuleType("cupy") + cupy_stub.zeros, cupy_stub.asarray, cupy_stub.arange = np.zeros, np.asarray, np.arange + cupy_stub.einsum, cupy_stub.asnumpy = np.einsum, lambda x: x + with mock.patch.dict("sys.modules", {"cupy": cupy_stub}): + sigma_gpu = nonlocal_sde.calculate_sigma_from_kernel_gpu(make_kernel(), giwk, q_list) + with mock.patch.dict("sys.modules", {"cupy": None}): + sigma_auto = nonlocal_sde.calculate_sigma_from_kernel_auto(mock.MagicMock(), make_kernel(), giwk, q_list) + + assert np.allclose(sigma_cpu.mat, sigma_ref.mat, atol=1e-6) + assert np.allclose(sigma_gpu.mat, sigma_ref.mat, atol=1e-6) + assert np.array_equal(sigma_auto.mat, sigma_cpu.mat) diff --git a/tests/test_nonlocal_sde_end_to_end.py b/tests/test_nonlocal_sde_end_to_end.py index 328dfb0d..eaae70b4 100644 --- a/tests/test_nonlocal_sde_end_to_end.py +++ b/tests/test_nonlocal_sde_end_to_end.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import contextlib @@ -165,7 +165,6 @@ def test_calculates_nonlocal_sde_correctly(setup, niw_core, niv_core, niv_shell, config.dmft.symmetrize_orbitals = [] config.memory.save_memory_for_chi0q = save_memory config.memory.save_memory_for_chiq_aux = save_memory - config.memory.save_memory_for_sde = save_memory g_dmft, s_dmft, g2_dens, g2_magn = tuple(x[0] for x in dga_io.load_from_dmft_file_and_update_config()) @@ -200,7 +199,6 @@ def test_calculates_srvo3_correctly(setup_srvo3_cubic, save_memory): config.memory.save_memory_for_chi0q = save_memory config.memory.save_memory_for_chiq_aux = save_memory - config.memory.save_memory_for_sde = save_memory config.output.output_path = folder_cubic diff --git a/tests/test_self_energy.py b/tests/test_self_energy.py index ee332023..96055833 100644 --- a/tests/test_self_energy.py +++ b/tests/test_self_energy.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems from unittest.mock import MagicMock @@ -82,8 +82,8 @@ def test_fits_smom_algorithm_correctly_with_dummy_data(has_compressed_q_dimensio ) self_energy.mat = dummy_data # Assign dummy data to the matrix smom0, smom1 = self_energy.fit_smom() - assert np.allclose(smom0, dummy_smom0, rtol=1e-2) - assert np.allclose(smom1, dummy_smom1, rtol=1e-2) + assert np.allclose(smom0, dummy_smom0, atol=1e-2) + assert np.allclose(smom1, dummy_smom1, atol=1e-2) def test_fits_smom_correctly_with_edge_case_data(): @@ -125,7 +125,7 @@ def test_returns_correct_asymptotic_self_energy(custom_niv, n_min): ] asympt = self_energy._get_asympt(niv=niv, n_min=n_min) - assert np.allclose(asympt.mat, asympt_expected, rtol=1e-2) + assert np.allclose(asympt.mat, asympt_expected, atol=1e-2) def test_asympt_returns_self_energy_unchanged_when_core_equals_niv(): @@ -248,7 +248,7 @@ def test_interpolate_returns_same_values_when_beta_and_grid_are_unchanged(): result = self_energy.interpolate(beta_target=beta, niv_target=self_energy.niv) assert result.mat.shape == self_energy.mat.shape - assert np.allclose(result.mat, self_energy.mat, rtol=1e-6, atol=1e-6) + assert np.allclose(result.mat, self_energy.mat, atol=1e-6) def test_interpolate_reproduces_linear_frequency_dependence_on_a_new_grid(): @@ -270,7 +270,7 @@ def test_interpolate_reproduces_linear_frequency_dependence_on_a_new_grid(): expected = np.broadcast_to(expected_signal, result.mat.shape).copy() assert result.mat.shape == expected.shape - assert np.allclose(result.mat, expected, rtol=1e-4, atol=1e-4) + assert np.allclose(result.mat, expected, atol=1e-4) def _build_linear_self_energy(niv_value: int, beta_value: float, has_compressed_q_dimension: bool) -> SelfEnergy: @@ -362,7 +362,7 @@ def test_fits_polynomial_coefficients_correctly_with_default_parameters(): mat = np.full(mat.shape, f_vn + 1j * f_vn) # Dummy data for testing self_energy = _se(mat, nk=nk, has_compressed_q_dimension=False) result = self_energy.fit_polynomial(n_fit=25, degree=2) - assert np.allclose(result.mat[0, 0, 0], f_vn + 1j * f_vn, rtol=1e-2, atol=1e6) + assert np.allclose(result.mat[0, 0, 0], f_vn + 1j * f_vn, atol=1e6) def test_create_with_asympt_up_to_core_does_not_mutate_self(): diff --git a/tests/test_symmetrize.py b/tests/test_symmetrize.py index 5547b29e..cf4b7964 100644 --- a/tests/test_symmetrize.py +++ b/tests/test_symmetrize.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import pytest diff --git a/tests/test_symmetry_reduction.py b/tests/test_symmetry_reduction.py index d1d1e204..a26c0418 100644 --- a/tests/test_symmetry_reduction.py +++ b/tests/test_symmetry_reduction.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 Julian Peil # SPDX-License-Identifier: MIT # -# DGAmore — Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & +# DGAmore - Multi-Orbital Ladder Dynamical Vertex Approximation (LDGA) & # Eliashberg Equation Solver for Strongly Correlated Electron Systems import builtins @@ -852,7 +852,7 @@ def test_auto_discovery_finds_smaller_group_for_anisotropic_lattice(): @pytest.mark.slow def test_auto_discovery_matches_legacy_for_12cubed_cubic_hamiltonian(): - """Auto-discovered IBZ partition must match the legacy three_dimensional_cubic partition for a genuinely cubic 3-band Hamiltonian. (12^3 — slower.)""" + """Auto-discovered IBZ partition must match the legacy three_dimensional_cubic partition for a genuinely cubic 3-band Hamiltonian. (12^3 - slower.)""" import dgamore.brillouin_zone as bz fname, shape = "hk_3band_srvo3_cubic_12x12x12.npy", (12, 12, 12, 3, 3) @@ -870,7 +870,7 @@ def test_auto_discovery_matches_legacy_for_12cubed_cubic_hamiltonian(): @pytest.mark.slow def test_auto_discovery_matches_legacy_for_20cubed_cubic_hamiltonian(): - """Same as above for the 20^3 grid. (Even slower — covers the larger-grid path.)""" + """Same as above for the 20^3 grid. (Even slower - covers the larger-grid path.)""" import dgamore.brillouin_zone as bz fname, shape = "hk_3band_srvo3_cubic_20x20x20.npy", (20, 20, 20, 3, 3) @@ -1016,7 +1016,7 @@ def test_get_symmetry_reduction_default_excludes_antiunitary_ops(): """The default behaviour must drop anti-unitary operations; therefore no FBZ point should carry conj=True. This is the safe semantics for frequency- dependent quantities.""" H = _make_real_cubic_h(4, 4, 4, 1) result = sr.get_symmetry_reduction(H, atol=1e-8) - assert result["conjs"].any() == False # noqa: E712 — explicit bool check + assert result["conjs"].any() == False # noqa: E712 - explicit bool check def test_get_symmetry_reduction_include_antiunitary_admits_conj_ops(): @@ -1035,7 +1035,7 @@ def test_get_symmetry_reduction_include_antiunitary_shrinks_or_equals_ibz(): def test_get_symmetry_reduction_include_antiunitary_reconstructs_H_correctly(): - """When anti-unitary ops are included, reconstruction of H itself is still correct (anti-unitary ops are valid symmetries of H — only frequency-dependent objects are affected by the missing freq flip).""" + """When anti-unitary ops are included, reconstruction of H itself is still correct (anti-unitary ops are valid symmetries of H - only frequency-dependent objects are affected by the missing freq flip).""" H = _make_real_cubic_h(4, 4, 4, 1) result = sr.get_symmetry_reduction(H, atol=1e-8, include_antiunitary=True) H_ibz = H.reshape(-1, 1, 1)[result["irrk_ind"]] @@ -1063,7 +1063,7 @@ def test_get_symmetry_reduction_default_yields_no_conjugation_in_expansion(): """Concrete consequence of default ``include_antiunitary=False``: applying ``expand`` to any IBZ payload does NOT conjugate orbital indices anywhere. We verify this by feeding a complex payload built so that conjugation would be detectable (the conjugate differs from the original).""" H = _make_real_cubic_h(4, 4, 4, 1) result = sr.get_symmetry_reduction(H, atol=1e-8) - # Reconstruct H itself — well-defined and exact + # Reconstruct H itself - well-defined and exact H_ibz = H.reshape(-1, 1, 1)[result["irrk_ind"]] H_rec = result["expand"](H_ibz) assert np.allclose(H_rec, H, atol=1e-12) @@ -1203,9 +1203,7 @@ def boom(*a, **k): raise np.linalg.LinAlgError("forced") monkeypatch.setattr(sr.np.linalg, "svd", boom) - out = sr._fix_gauge_degenerate( - np.eye(2, dtype=complex), np.eye(2, dtype=complex), [[0, 1]], Hk, Hg, atol=1e-12 - ) + out = sr._fix_gauge_degenerate(np.eye(2, dtype=complex), np.eye(2, dtype=complex), [[0, 1]], Hk, Hg, atol=1e-12) assert out is None @@ -1227,9 +1225,7 @@ def flaky_svd(a, *args, **kwargs): raise np.linalg.LinAlgError("forced on block") monkeypatch.setattr(sr.np.linalg, "svd", flaky_svd) - out = sr._fix_gauge_degenerate( - np.eye(2, dtype=complex), np.eye(2, dtype=complex), [[0, 1]], Hk, Hg, atol=1e-12 - ) + out = sr._fix_gauge_degenerate(np.eye(2, dtype=complex), np.eye(2, dtype=complex), [[0, 1]], Hk, Hg, atol=1e-12) assert out is None assert state["n"] >= 2 @@ -1332,9 +1328,7 @@ def test_group_element_with_near_zero_U_skips_phase_normalization(): def _rot2(theta): - return np.array( - [[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]], dtype=np.complex128 - ) + return np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]], dtype=np.complex128) def test_apply_auto_orbital_transform_four_orbital_nonidentity_einsum(): @@ -1380,4 +1374,4 @@ def test_apply_auto_orbital_transform_reuses_cached_path_four_dim(): u = us[i] uc = u.conj() exp = np.einsum("ap,bq,cr,ds,pqrs->abcd", u, uc, u, uc, mat[i]) - assert np.allclose(out[i], exp) \ No newline at end of file + assert np.allclose(out[i], exp)