Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 5 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions .github/workflows/Docs.yml
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions .github/workflows/Lint.yml
Original file line number Diff line number Diff line change
@@ -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 .
14 changes: 8 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ coverage:
default:
target: 85%
threshold: 0%
informational: true
informational: false
comment:
layout: "reach, diff, flags, files"
behavior: default
54 changes: 37 additions & 17 deletions dgamore/DGAmore.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
)
Expand All @@ -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],),
Expand Down Expand Up @@ -581,13 +585,27 @@ def autodetect_memory_settings(comm: MPI.Comm) -> None:
construct_fq_cheap=config.eliashberg.construct_fq_cheap,
)

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
# giwk_full is deduplicated to one copy per node through the whole non-local self-energy section when
# config.memory.use_shared_memory_giwk is on (the node root builds it, the others map it read-only; it is re-shared
# at the smaller core-box window after the bubble).
giwk_shared_credit = (
memory_estimator.giwk_baseline_bytes(config.lattice.q_grid.nk_tot, config.sys.n_bands, niv_cut)
if config.memory.use_shared_memory_giwk
else 0.0
)
giwk_shared_branches = {"chi0q", "chiq_aux", "sde"}

def node_total(distributed: float, single: float, n_ranks: int, giwk_shared: bool = False) -> float:
"""Memory held on a node with ``n_ranks`` ranks at a branch's peak (see :func:`autodetect_memory_settings`).
When ``giwk_shared``, the replicated ``giwk_full`` is counted once per node instead of once per rank."""
total = n_ranks * (baseline + distributed) + single
if giwk_shared:
total -= (n_ranks - 1) * giwk_shared_credit
return total

def fits_everywhere(distributed: float, single: float) -> bool:
def fits_everywhere(distributed: float, single: float, giwk_shared: bool = False) -> 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(distributed, single, r, giwk_shared) <= avail * 0.9 for r, avail in nodes.values())

flag_to_key = {
"save_memory_for_chi0q": "chi0q",
Expand All @@ -613,17 +631,19 @@ def fits_everywhere(distributed: float, single: float) -> bool:
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())
# giwk is deduplicated per node across the whole SDE section, so credit those branches (not the eliashberg ones).
giwk_shared = key in giwk_shared_branches and config.memory.use_shared_memory_giwk
if not fits_everywhere(bp.on_distributed, bp.on_single, giwk_shared):
worst = max(node_total(bp.on_distributed, bp.on_single, r, giwk_shared) 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."
)
autodetect_on = not fits_everywhere(bp.off_distributed, bp.off_single)
autodetect_on = not fits_everywhere(bp.off_distributed, bp.off_single, giwk_shared)
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.off_distributed, bp.off_single, r, giwk_shared) 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'}."
Expand Down Expand Up @@ -702,4 +722,4 @@ def configure_matplotlib():


if __name__ == "__main__":
execute_dga_routine()
main()
3 changes: 1 addition & 2 deletions dgamore/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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

4 changes: 2 additions & 2 deletions dgamore/brillouin_zone.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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

"""
Expand Down Expand Up @@ -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()."""
Expand Down
2 changes: 1 addition & 1 deletion dgamore/bubble_gen.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand Down
6 changes: 5 additions & 1 deletion dgamore/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand Down Expand Up @@ -278,6 +278,9 @@ class MemoryConfig:
: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):
Expand All @@ -286,6 +289,7 @@ def __init__(self):
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:
Expand Down
3 changes: 2 additions & 1 deletion dgamore/config_parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand Down Expand Up @@ -326,6 +326,7 @@ def _build_memory_config(self, config_file):
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

Expand Down
4 changes: 3 additions & 1 deletion dgamore/dga_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,6 @@ memory:
# 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.
# 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)
2 changes: 1 addition & 1 deletion dgamore/dga_io.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand Down
2 changes: 1 addition & 1 deletion dgamore/dga_logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand Down
2 changes: 1 addition & 1 deletion dgamore/dmft_interface.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2025-2026 Julian Peil <julian.peil@tuwien.ac.at>
# 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
Expand Down
Loading
Loading