Skip to content

Add dense/recursive NEGF switch, audit performance, parallelize with rayon - #2

Merged
reneotten merged 1 commit into
claude/rust-rewrite-python-frontend-a70idsfrom
claude/negf-dense-recursive-switch-parallel
Jul 25, 2026
Merged

Add dense/recursive NEGF switch, audit performance, parallelize with rayon#2
reneotten merged 1 commit into
claude/rust-rewrite-python-frontend-a70idsfrom
claude/negf-dense-recursive-switch-parallel

Conversation

@reneotten

Copy link
Copy Markdown
Owner

Summary

Builds on #1 (not yet merged, hence that branch as the base here).

  • Dense/recursive switch: the NEGF engine previously only had the O(N) recursive algorithm. Promoted the dense O(N³) reference (previously test-only) to a real, selectable algorithm (GreenFunctionAlgorithm::{Recursive, Dense} in Rust, algorithm="recursive"|"dense" in Python) so both are available and validated against each other — dense as an easy-to-trust reference / fallback, recursive as the fast default.
  • Performance audit + parallelism: added crates/negforge-core/examples/benchmark.rs and used it to find and exploit the real parallelism in this problem:
    • The NEGF energy loop is embarrassingly parallel (each energy point only reads the same fixed Hamiltonian) — parallelized with rayon, benefiting both algorithms.
    • Bias sweeps (sweep_v_g/sweep_v_ds) are parallel per bias point, since set_v_g/set_v_ds already reset device state from scratch — nothing carries over between points. This changes their signature from &mut Device to &Device (each point runs on its own clone), which also fixes a surprising side effect where a sweep used to silently leave the device parked at its last bias point.
    • Self-consistent loop iterations remain sequential (each depends on the previous iteration's potential) — documented as the one thing that genuinely can't be parallelized here.
  • README gets a new "Performance" section with the tradeoff explanation and real benchmark numbers (dense scales ~cubically, recursive is flat; parallel energy loop gets ~3-4x on a 4-core machine).
  • Notebook gets a new section demonstrating the switch live (small device, both algorithms agree to ~1e-12, dense is measurably slower even at N=31).

Test plan

  • cargo test --workspace — 19 tests pass, including new cross-checks: negf::tests::dense_and_recursive_algorithms_agree_end_to_end, selfconsistent::tests::dense_and_recursive_converge_to_the_same_potential, sweep::tests::sweep_does_not_mutate_the_input_device
  • cargo clippy --workspace --all-targets — clean
  • cargo run --release --example benchmark -p negforge-core — produces the numbers quoted in the README
  • maturin develop --release rebuilds; verified algorithm="dense"/"recursive" give matching results at different speeds, and an invalid algorithm string raises ValueError
  • notebooks/negforge_demo.ipynb re-executed end-to-end with the new section included, no errors

Generated by Claude Code

Explains and implements both NEGF algorithms behind a runtime switch
(GreenFunctionAlgorithm::{Recursive, Dense} in Rust, algorithm=
"recursive"|"dense" in Python), rather than only having the O(N)
recursive path:

- Dense: literal O(N^3) full-matrix inversion, matching the original
  MATLAB inv() call. Simple to trust (no tridiagonal-structure
  assumption, no recursion formula to get wrong), so it now doubles as
  the reference the recursive path is validated against, and as a
  fallback if the Hamiltonian ever gains longer-range hopping.
- Recursive (default): the existing O(N) left-connected recursive
  Green's-function sweep. The one to use for anything performance-
  sensitive.

Audited performance and parallelized where the structure actually
allows it:
- The NEGF energy loop is embarrassingly parallel (each energy point
  only reads the same fixed Hamiltonian) -- parallelized with rayon,
  benefiting both algorithms.
- Bias sweeps (sweep_v_g/sweep_v_ds) are parallel per bias point, since
  set_v_g/set_v_ds already reset device state from scratch. This
  changes their signature from &mut Device to &Device (each point runs
  on its own clone), which also fixes a surprising side effect where a
  sweep silently left the device parked at its last bias point.
- Self-consistent loop iterations remain sequential (each depends on
  the previous iteration's potential) -- documented as the one thing
  that can't be parallelized here.

Added crates/negforge-core/examples/benchmark.rs with wall-clock
numbers (dense vs. recursive scaling, parallel vs. serial speedup),
quoted in the README's new "Performance" section. Added tests cross-
validating both algorithms against each other (single sweep and
through the full self-consistent loop) and confirming sweeps no longer
mutate the input device. Notebook updated with a live dense-vs-
recursive comparison cell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVNo3bky8MhMCth48hZT4m

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a selectable NEGF Green’s-function backend (dense reference vs. recursive fast path) and introduces Rayon-based parallelism over independent energy points and bias-sweep points, with corresponding updates to the Rust core, PyO3 bindings, Python convenience wrapper, benchmark tooling, and documentation/notebook examples.

Changes:

  • Introduces GreenFunctionAlgorithm::{Recursive, Dense} and wires it through NEGF sweeps and the self-consistent solver (Rust + Python API surface).
  • Parallelizes NEGF energy-point evaluation and bias sweeps using rayon, and makes Rust sweep APIs non-mutating (&Device).
  • Adds a benchmark example and expands README/notebook documentation with performance guidance and usage examples.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
README.md Documents the dense/recursive switch, parallelism approach, and benchmark numbers.
python/negforge/__init__.py Exposes algorithm= in the Python wrapper for self-consistent solves, LDOS, and sweeps.
notebooks/negforge_demo.ipynb Demonstrates dense vs. recursive behavior and timing in the notebook flow.
crates/negforge-py/src/lib.rs Implements Python-facing parsing/validation of algorithm and updates bindings to new Rust APIs.
crates/negforge-core/src/sweep.rs Makes sweeps non-mutating and parallel per bias point via cloning + Rayon.
crates/negforge-core/src/selfconsistent.rs Threads the algorithm selection into the self-consistent loop via options.
crates/negforge-core/src/negf.rs Adds GreenFunctionAlgorithm, dense backend, and parallel energy loop execution.
crates/negforge-core/src/lib.rs Re-exports GreenFunctionAlgorithm publicly.
crates/negforge-core/examples/benchmark.rs Adds a benchmark harness for algorithm and parallelism comparisons.
crates/negforge-core/Cargo.toml Adds rayon dependency for parallel execution.
Cargo.lock Locks new transitive dependencies introduced by rayon.
Comments suppressed due to low confidence (1)

crates/negforge-py/src/lib.rs:206

  • Same issue as sweep_v_g: algorithm is parsed even when self_consistent=false, so invalid strings raise ValueError despite being documented as irrelevant unless self-consistency is enabled. Only parse/validate when self_consistent is true.
        let opts = SelfConsistentOptions {
            algorithm: parse_algorithm(algorithm)?,
            ..Default::default()
        };
        let sc = if self_consistent { Some(&opts) } else { None };
        let points = negforge_core::sweep::sweep_v_ds(&self.inner, v_min, v_max, step, sc)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/negforge-py/src/lib.rs
reneotten pushed a commit that referenced this pull request Jul 25, 2026
Follow-up to the review comments on #1 and #2.

- tridiag: `solve_real`/`solve_complex` no longer panic on degenerate
  sizes — `n == 0` now asserts with a clear message and `n == 1` is
  handled as a scalar division instead of indexing empty off-diagonals.
  Also removed a dead bounds check in the forward sweep. `Device` asserts
  up front that the grid spacing yields at least two points, so a coarse
  `a` fails with an explanation rather than an index panic.

- device: `set_v_ds`/`set_v_g`/`set_l_ch` (and `Device::new`) now re-solve
  the electrostatic potential, so `psi_f`/`psi_0` can never be left over
  from the previous bias point. Callers no longer have to remember an
  explicit `calc_potential()` before `calc_current()` or the NEGF entry
  points.

- sweep: `inclusive_range` truncates instead of rounding the step count,
  so a span that isn't a whole multiple of the step stops inside the
  range instead of overshooting past `v_max`; ranges within floating-point
  noise of a whole multiple (0.4/0.05) still keep their endpoint. Invalid
  ranges (non-positive/non-finite step, reversed bounds) are now errors.

- sweep/python: `subthreshold_swing` validates that the currents in the
  fit window are strictly positive and finite before taking `log10`,
  instead of silently fitting `-inf`/`nan`, and rejects a degenerate
  (flat-current) fit.

- python bindings: engine `InvalidParameter` errors now surface as
  `ValueError` rather than `RuntimeError`, matching the `algorithm`
  validation. Documented why `algorithm` is validated even when
  `self_consistent=False` (catching a typo'd name is the point).

cargo test --workspace: 24 tests pass; cargo clippy --workspace
--all-targets clean; Python API smoke-tested against a maturin build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf4rT4UcjJtZgA7Meiq3Tc
@reneotten
reneotten merged commit 4ae294f into claude/rust-rewrite-python-frontend-a70ids Jul 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants