Skip to content

Add planar/FinFET/nanoribbon device geometry support - #3

Merged
reneotten merged 1 commit into
claude/negf-dense-recursive-switch-parallelfrom
claude/multigate-geometry-support
Jul 25, 2026
Merged

Add planar/FinFET/nanoribbon device geometry support#3
reneotten merged 1 commit into
claude/negf-dense-recursive-switch-parallelfrom
claude/multigate-geometry-support

Conversation

@reneotten

Copy link
Copy Markdown
Owner

Summary

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

The natural-length electrostatics already had a geo (number-of-gates) factor — exactly how compact 1D models distinguish planar, multi-gate, and gate-all-around architectures (lambda ~ 1/sqrt(geo)) — but it was an undocumented, unvalidated raw float with no way to construct a realistic device of a given architecture.

  • Added GateGeometry (SingleGate/DoubleGate/TriGate/GateAllAround) and DeviceParams::{planar, fin_fet, nanoribbon} presets with realistic body/oxide dimensions per architecture. Mirrored as negforge.Device.planar()/fin_fet()/nanoribbon() in Python.
  • Validation fix: Device::new now rejects non-positive/non-finite geo, d_ch, d_ox, k_si, k_ox, a, l_ch, l_ds instead of silently producing an infinite/NaN natural length that propagates garbage through the rest of the solve — a real risk now that geo is something callers set explicitly. Added a fallible Device::try_new; the PyO3 constructor uses it and now raises a proper Python ValueError (catchable by except Exception) instead of an uncaught-by-normal-exception-handling Rust panic.
  • Verified the physics, not just "doesn't crash": crates/negforge-core/tests/multigate_geometry.rs + examples/geometry_comparison.rs confirm natural-length ordering, all three architectures converging (decoupled and self-consistent), and the textbook multi-gate result — at l_ch=10nm, planar shows real short-channel-effect degradation (147 mV/decade subthreshold swing) while FinFET (86) and especially gate-all-around nanoribbon (64, close to the 59.6 mV/decade ideal limit) don't.
  • Notebook gets a new section plotting potential profiles and subthreshold swing across all three architectures.
  • README gets a new "Planar, FinFET and nanoribbon devices" section, plus an honest "Extending to a real 3D solver" section: the optional 3D extension was not implemented. A genuine cross-section-resolved model (mode-space NEGF, the standard approach real nanoscale-FET simulators use) is weeks-to-months of separate numerical-methods and validation work — a different piece of software, not an incremental change — and is scoped out there rather than attempted as a shallow, hard-to-trust addition. Happy to scope that as its own effort if wanted.

Test plan

  • cargo test --workspace — 27 tests pass, including 6 new geometry tests and 2 new validation-panic tests
  • cargo clippy --workspace --all-targets — clean
  • cargo run --release --example geometry_comparison -p negforge-core — reproduces the numbers quoted in the README
  • maturin develop --release rebuilds; verified the presets match Rust exactly, and invalid geometry raises ValueError (catchable by both except ValueError and except Exception)
  • notebooks/negforge_demo.ipynb re-executed end-to-end with the new section, no errors

Generated by Claude Code

The natural-length electrostatics already had a `geo` (number-of-gates)
factor that is exactly how compact 1D models distinguish planar,
multi-gate, and gate-all-around architectures (lambda ~ 1/sqrt(geo)) --
but it was an undocumented, unvalidated raw float with no way to
construct a realistic device of a given architecture.

- Added GateGeometry (SingleGate/DoubleGate/TriGate/GateAllAround) and
  DeviceParams::{planar, fin_fet, nanoribbon} presets with realistic
  body/oxide dimensions per architecture.
- Added validation: Device::new now panics (via a new fallible
  Device::try_new) on non-positive/non-finite geo, d_ch, d_ox, k_si,
  k_ox, a, l_ch, or l_ds -- previously a bad value silently produced an
  infinite/NaN natural length and propagated garbage through the rest
  of the solve. The PyO3 constructor now surfaces this as a proper
  Python ValueError (via a new InvalidParameter-aware error mapping)
  instead of an uncatchable-by-`except Exception` panic exception.
- Verified the physics: added
  crates/negforge-core/tests/multigate_geometry.rs and
  examples/geometry_comparison.rs confirming natural-length ordering,
  that all three architectures converge (decoupled and
  self-consistent), and the textbook multi-gate result -- at a short
  channel length, planar shows real short-channel-effect degradation
  (147 mV/decade subthreshold swing) while FinFET (86) and especially
  gate-all-around nanoribbon (64, close to the 59.6 mV/decade ideal
  limit) don't.
- Exposed matching presets in the Python wrapper
  (negforge.Device.planar/fin_fet/nanoribbon) and added a notebook
  section demonstrating them.
- README: new "Planar, FinFET and nanoribbon devices" section, plus an
  honest "Extending to a real 3D solver" section scoping out what a
  genuine cross-section-resolved (mode-space NEGF) model would require
  and why it's a separate undertaking from this pass.

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

Adds explicit planar/FinFET/nanoribbon “multigate geometry” support to the 1D device model by formalizing the gate-count (geo) concept, providing architecture presets, and tightening parameter validation (including Python error mapping) to prevent NaN/∞ electrostatics from silently propagating.

Changes:

  • Introduces GateGeometry and DeviceParams::{planar, fin_fet, nanoribbon} (plus a Rust example + tests) to model planar vs multi-gate architectures via geo.
  • Makes device construction validation-fail early via Device::try_new and maps invalid-parameter errors to Python ValueError in the PyO3 binding.
  • Updates README and the demo notebook with architecture comparisons and documented limitations of the 1D model.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Documents gate-geometry presets and adds an architecture comparison + 3D-solver scope notes.
python/negforge/init.py Adds Python Device.planar/fin_fet/nanoribbon convenience constructors.
notebooks/negforge_demo.ipynb Adds a section plotting potential profiles and subthreshold swing across architectures.
crates/negforge-py/src/lib.rs Switches PyO3 constructor to fallible try_new and maps invalid params to ValueError.
crates/negforge-core/tests/multigate_geometry.rs Adds physics/robustness tests for the new presets and expected trends.
crates/negforge-core/src/lib.rs Re-exports GateGeometry from the core crate.
crates/negforge-core/src/device.rs Adds GateGeometry, architecture presets, and parameter validation + try_new.
crates/negforge-core/examples/geometry_comparison.rs Adds a runnable example that prints the architecture comparison used in docs.

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

Comment on lines +78 to +83
@classmethod
def planar(cls, **overrides) -> "Device":
"""A planar, single-gate (bulk or SOI) MOSFET. Same defaults as
`Device()` -- planar/single-gate is this model's baseline."""
return cls(geo=1.0, **overrides)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed — cls(geo=1.0, **{"geo": 2.0}) is a TypeError at call time, so Device.planar(geo=2.0) cannot work while fin_fet(geo=3.0) and nanoribbon(geo=...) can. The docstring on fin_fet even advertises overriding geo ("Pass geo=3.0 to also gate the fin top"), which makes the inconsistency more surprising rather than less.

The fix is to make planar match its siblings:

params = {"geo": 1.0}
params.update(overrides)
return cls(**params)

Not changing it here — this is #3's branch and I'm working on #5 — flagging it as confirmed for whoever picks this up.


Generated by Claude Code

Comment on lines +293 to 318
fn validate_params(params: &DeviceParams) -> crate::error::Result<()> {
let positive = [
("geo", params.geo),
("d_ch", params.d_ch),
("d_ox", params.d_ox),
("k_si", params.k_si),
("k_ox", params.k_ox),
("a", params.a),
("l_ch", params.l_ch),
];
for (name, value) in positive {
if !(value.is_finite() && value > 0.0) {
return Err(crate::error::NegForgeError::InvalidParameter(format!(
"{name} must be finite and positive, got {value}"
)));
}
}
let l_ds_ok = params.l_ds.is_finite() && params.l_ds > 0.0;
if !params.auto_size_contacts && !l_ds_ok {
return Err(crate::error::NegForgeError::InvalidParameter(format!(
"l_ds must be finite and positive when auto_size_contacts is false, got {}",
params.l_ds
)));
}
Ok(())
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Confirmed, and the d_e case is worse than "unbounded loop" suggests — it's a hang, not an error.

calc_current computes ((e_max - psi_0) / d_e).floor().max(0.0) as usize and then iterates 0..=n_steps. Rust's float-to-int casts saturate, so d_e = 0.0 gives inf as usize == usize::MAX and the loop runs ~1.8e19 iterations with no allocation and no panic to stop it. epsilon = 0.0 reaches the same place by a different route: epsilon.ln() is -inf, so e_max is +inf. The same pattern appears in negf_current and in the self-consistent loop's energy grid.

t <= 0 is less dramatic but still wrong: it divides by zero in the Fermi functions, giving f = 0 everywhere and a silent zero current rather than an error.

So the three additions to validate_params are worth having, with epsilon needing 0 < epsilon < 1 specifically (it is a Fermi-function tolerance, and ln of anything >= 1 puts e_max at or below e_fs, collapsing the integration window). m_eff is the fourth of its kind — it divides into t_hop.

Not changing it here (this is #3's branch), but noting for sequencing: #5 will be rebased on top of this branch, so validate_params is where its grid-size and cross-section checks will land too.


Generated by Claude Code

reneotten pushed a commit that referenced this pull request Jul 25, 2026
Rebased onto #3's branch, since that lands first — its `try_new`/
`validate_params` is the natural home for the validation this needs, and
the two would otherwise fix the same panic twice.

- Construction no longer panics on user-supplied parameters. The grid-size
  check moves out of `compute_grid`'s `assert!` into a `validate_grid`
  step inside `try_new`, run after `l_ds` may have been auto-sized from
  `lambda` (which is why it can't live in `validate_params` with the
  rest). `cross_section_nm2` is validated alongside. `compute_grid` keeps
  a `debug_assert!` for internal misuse.
- `set_l_ch` is fallible for the same reason: it re-derives the grid, so it
  can be handed a length that cannot be discretized. Validation runs on a
  copy, so a rejected value leaves the device untouched.
- `IVCurve` rejects mismatched or non-1-D arrays instead of letting them
  surface later as a confusing masking error or a silently misaligned
  curve.
- `calc_potential`'s docstring no longer suggests mutating `rho` from
  Python: the property returns a copy, so that never worked. Says what is
  true instead, and `rho`'s own docstring now states it is a snapshot and
  gives its units.

Rebase adjustments to #3's code: `subthreshold_swing` returns `Result` now,
so its two call sites unwrap; `multigate_geometry.rs` drops its explicit
`eta: 0.05`, which is exactly the new `5 * d_e` default at that device's
`d_e = 0.01`.

cargo test --workspace: 46 tests pass; cargo clippy --workspace
--all-targets clean; notebook re-executed end to end (both this branch's
NEGF section and #3's geometry section) with no errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pf4rT4UcjJtZgA7Meiq3Tc
@reneotten
reneotten merged commit ad43302 into claude/negf-dense-recursive-switch-parallel 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