diff --git a/README.md b/README.md index 5d58a82..e353c39 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ program. `negforge-py` is a thin binding layer on top of it. - `tridiag` — real and complex Thomas-algorithm tridiagonal solvers, used by both the electrostatics and NEGF modules. - `device` — device geometry/parameters and the electrostatic - (`calc_potential`) and ballistic-current (`calc_current`) calculations. + (`calc_potential`) and ballistic-current (`calc_current`) calculations, + including the `GateGeometry`/`planar`/`fin_fet`/`nanoribbon` presets — + see "Planar, FinFET and nanoribbon devices" below. - `negf` — the NEGF retarded Green's-function calculation, with two interchangeable algorithms (`GreenFunctionAlgorithm::{Recursive, Dense}`) and a parallel energy loop — see "Performance" below. @@ -93,6 +95,89 @@ screened contact regions: Units follow the original code throughout: lengths in nm, energies and potentials in eV, temperature in Kelvin. +## Planar, FinFET and nanoribbon devices + +`geo` in the natural-length formula above (`lambda = sqrt(k_si/k_ox * +d_ch * d_ox / geo)`) is the number of gates electrostatically controlling +the channel — the "generalized scale length" model from the multi-gate +MOSFET literature (Auth & Plummer 1997). It's the one parameter that +actually distinguishes device architectures in this 1D model: more gates +gives a smaller natural length, i.e. tighter electrostatic control and +better short-channel-effect immunity, for the same body thickness. +[`GateGeometry`] enumerates the standard cases (`SingleGate`=1, +`DoubleGate`=2, `TriGate`=3, `GateAllAround`=4), and `DeviceParams` has +matching presets with realistic body/oxide dimensions: + +| Preset (Rust `DeviceParams::`, Python `negforge.Device.`) | Architecture | `geo` | Notes | +|---|---|---|---| +| `planar()` | Planar bulk/SOI MOSFET | 1 | Same as `default()` | +| `fin_fet()` | Double-gate FinFET | 2 | Thin fin (`d_ch` = fin width), sidewall gates only; `.with_gate_geometry(GateGeometry::TriGate)` if the fin top is also gated | +| `nanoribbon()` | Gate-all-around nanoribbon/nanowire | 4 | Narrow body (`d_ch` = ribbon width/diameter), gate wraps all sides | + +This reproduces the textbook result +(`crates/negforge-core/examples/geometry_comparison.rs`, cross-checked in +`crates/negforge-core/tests/multigate_geometry.rs`): at a channel length +short enough for the planar device to show real short-channel-effect +degradation, FinFET and especially gate-all-around do much better — +subthreshold swing at `l_ch = 10 nm` (ideal thermal limit at 300 K is +59.6 mV/decade): + +``` +planar lambda= 8.473 nm S= 147.3 mV/decade +fin_fet lambda= 4.151 nm S= 86.4 mV/decade +nanoribbon lambda= 1.895 nm S= 64.3 mV/decade +``` + +All three architectures also run through the full self-consistent +Poisson↔NEGF loop and both NEGF algorithms without issue — see the +test file above. + +**What this is not**: `d_ch` here is a single effective body-thickness +number, not an actually-resolved cross-section — there's no transverse- +mode/subband quantization, so it can't capture, say, the difference +between a wide-and-thin FinFET fin and a narrow-and-thick one with the +same `geo` and cross-sectional area, or volume-inversion effects specific +to very narrow gate-all-around wires. Modeling that requires resolving the +cross-section, which is what a real 3D (or mode-space) extension would +add — see below. + +## Extending to a real 3D solver + +Nothing here — every geometry preset above still uses the same 1D +natural-length electrostatics and 1D NEGF transport. A genuine 3D (or +"mode-space", the standard middle ground) solver is a substantially +different piece of software, not an incremental change to this one, and +wasn't built as part of this pass. Rough shape of what it would take, for +context if this becomes a real ask later: + +1. **Mode-space NEGF** (the tractable approach real nanoscale-FET + simulators use, e.g. nanoMOS/OMEN/NEMO — as opposed to full real-space + 3D NEGF, which is a research-grade undertaking on its own): at each + slice along the transport direction, solve a 2D (FinFET) or + cross-sectional Schrodinger equation for the transverse confinement + eigenvalues (subbands) and eigenvectors, given the local potential. +2. Run this crate's existing 1D NEGF machinery once per subband, using + each subband's confinement energy as an added effective potential + floor, and sum the resulting charge/current across subbands. +3. Couple that to a 2D or 3D Poisson solve (cross-section x length, or a + full 3D mesh) instead of the current 1D natural-length formula, and + iterate to self-consistency the same way `selfconsistent.rs` already + does for the 1D case. +4. None of the current O(N) tridiagonal tricks carry over directly — the + Poisson operator is no longer tridiagonal in 2D/3D (a sparse + iterative solver, e.g. conjugate gradient, would replace the Thomas + algorithm), and the NEGF energy-loop parallelism generalizes but now + has a subband dimension to parallelize over too. + +This is weeks-to-months of numerical-methods and validation work, not a +follow-on patch, and getting it subtly wrong (e.g. a sign error in the +subband coupling) is easy to do and hard to notice without a trusted 2D/3D +reference to check against — unlike the 1D engine here, which could be +(and was) validated by cross-checking algorithms against each other and +against known physical trends. If this is wanted, it's worth scoping and +staffing as its own effort rather than folding into this codebase's +existing architecture. + ## Deviations from the original MATLAB code The user requested a faithful port plus closing the missing @@ -266,9 +351,18 @@ dev.sweep_v_g(0.0, 0.4, 0.05, self_consistent=True, algorithm="dense") integration test at the model's default (non-toy) device scale, confirming the self-consistent loop converges and reproduces the expected ballistic-MOSFET trend (current increasing with gate bias). +- `crates/negforge-core/tests/multigate_geometry.rs` — the + planar/FinFET/nanoribbon physical sanity checks: natural-length + ordering, all three architectures converging (decoupled and + self-consistent), and subthreshold swing improving with more gates at a + short channel length. - `crates/negforge-core/examples/benchmark.rs` — the performance audit behind the numbers quoted above; run it with `cargo run --release --example benchmark -p negforge-core`. +- `crates/negforge-core/examples/geometry_comparison.rs` — the + planar/FinFET/nanoribbon subthreshold-swing comparison quoted above; run + it with `cargo run --release --example geometry_comparison -p + negforge-core`. - `notebooks/negforge_demo.ipynb` has been executed end-to-end (`jupyter nbconvert --execute`) to confirm the full frontend path works, including the dense-vs-recursive comparison cell; outputs are cleared diff --git a/crates/negforge-core/examples/geometry_comparison.rs b/crates/negforge-core/examples/geometry_comparison.rs new file mode 100644 index 0000000..0be357a --- /dev/null +++ b/crates/negforge-core/examples/geometry_comparison.rs @@ -0,0 +1,36 @@ +//! Demonstrates the planar/FinFET/nanoribbon presets and the textbook +//! result they're expected to reproduce: at a fixed (short) channel +//! length, more gates means better electrostatic control and a +//! subthreshold swing closer to the ideal thermal limit +//! (`ln(10)*kT/e = 59.6 mV/decade` at 300 K). +//! +//! Run with `cargo run --release --example geometry_comparison -p +//! negforge-core`. Numbers from this example are quoted in the top-level +//! README's "Planar, FinFET and nanoribbon devices" section. + +use negforge_core::sweep::{subthreshold_swing, sweep_v_g}; +use negforge_core::{Device, DeviceParams}; + +fn report(name: &str, mut params: DeviceParams, l_ch: f64) { + params.l_ch = l_ch; + params.v_ds = 0.3; + let dev = Device::new(params); + let points = sweep_v_g(&dev, 0.0, 0.4, 0.02, None).unwrap(); + let s = subthreshold_swing(&points, 0.0, 0.4); + println!( + "{name:<11} lambda={:>6.3} nm S={:>6.1} mV/decade", + dev.lambda, + s * 1000.0 + ); +} + +fn main() { + println!("(ideal thermal limit at 300 K: 59.6 mV/decade)\n"); + for l_ch in [10.0, 20.0, 40.0] { + println!("=== l_ch = {l_ch} nm ==="); + report("planar", DeviceParams::planar(), l_ch); + report("fin_fet", DeviceParams::fin_fet(), l_ch); + report("nanoribbon", DeviceParams::nanoribbon(), l_ch); + println!(); + } +} diff --git a/crates/negforge-core/src/device.rs b/crates/negforge-core/src/device.rs index e948385..f15041a 100644 --- a/crates/negforge-core/src/device.rs +++ b/crates/negforge-core/src/device.rs @@ -15,10 +15,66 @@ //! temperature in Kelvin. `Psi_g`/`Psi_bi`/`Psi_f` store *potential energy* //! (already in eV), not electrostatic potential in volts — consistent with //! `E_f`/`E_g` also being in eV. +//! +//! ## Planar, FinFET and nanoribbon devices +//! +//! `lambda = sqrt(k_si/k_ox * d_ch * d_ox / geo)` is the standard +//! "generalized scale length" (natural length) model used throughout the +//! multi-gate MOSFET literature (Auth & Plummer 1997; see also the +//! Ferain/Colinge/Colinge review of multigate transistors). `geo` is the +//! number of gates electrostatically controlling the channel, and is the +//! one parameter that actually distinguishes a planar, FinFET, or +//! gate-all-around (nanoribbon) architecture in this model — more gates +//! means a smaller natural length, i.e. tighter electrostatic control and +//! better short-channel-effect immunity, for the same body thickness. See +//! [`GateGeometry`] and the `DeviceParams::planar`/`fin_fet`/`nanoribbon` +//! constructors below, and +//! `crates/negforge-core/tests/multigate_geometry.rs` for the physical +//! sanity checks (natural length ordering, subthreshold swing improving +//! with more gates). +//! +//! This is still a 1D model along the transport direction: `d_ch` is a +//! single effective body-thickness number (fin width, film thickness, or +//! ribbon diameter, depending on architecture) rather than an actual +//! resolved cross-section, and there's no transverse-mode/subband +//! quantization. See the top-level README's "Extending to a real 3D +//! solver" section for what a genuine cross-section-resolved model would +//! require. use crate::constants::{E, EPS_0, H_BAR, K_B, M_E}; use crate::tridiag; +/// Number of gates electrostatically controlling the channel, used to pick +/// `DeviceParams::geo` for a given device architecture. See the module +/// docs for the underlying scale-length model and its literature basis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GateGeometry { + /// One gate (planar bulk MOSFET, or a single-gate SOI film). + SingleGate, + /// Two gates (planar double-gate SOI, or a FinFET with an inactive — + /// e.g. hard-mask-capped — fin top, so only the two sidewalls conduct). + DoubleGate, + /// Three active gates (a FinFET with an active top gate as well as + /// both sidewalls). + TriGate, + /// The gate fully wraps the channel (gate-all-around nanowire / + /// nanoribbon), idealized as a rectangular cross-section with all four + /// sides gated. + GateAllAround, +} + +impl GateGeometry { + /// The `geo` (number-of-gates) factor for [`DeviceParams`]. + pub fn geo_factor(self) -> f64 { + match self { + GateGeometry::SingleGate => 1.0, + GateGeometry::DoubleGate => 2.0, + GateGeometry::TriGate => 3.0, + GateGeometry::GateAllAround => 4.0, + } + } +} + /// Configuration for a [`Device`]. Field defaults match the original /// `quantumsim.m` `properties` block. #[derive(Debug, Clone, Copy, PartialEq)] @@ -41,7 +97,9 @@ pub struct DeviceParams { pub k_si: f64, /// Relative permittivity of the oxide. pub k_ox: f64, - /// Gate geometry factor (number of gates). + /// Gate geometry factor (number of gates). See [`GateGeometry`] and + /// the module docs for how this distinguishes planar/FinFET/nanoribbon + /// architectures. pub geo: f64, /// Channel length, nm. pub l_ch: f64, @@ -93,6 +151,52 @@ impl Default for DeviceParams { } } +impl DeviceParams { + /// Set `geo` from a [`GateGeometry`], leaving every other field + /// unchanged. Note this only changes the *number of gates*; a real + /// FinFET or nanoribbon also has a much thinner body than a planar + /// device — see `fin_fet()`/`nanoribbon()` below for presets that set + /// realistic dimensions too. + pub fn with_gate_geometry(mut self, geometry: GateGeometry) -> Self { + self.geo = geometry.geo_factor(); + self + } + + /// Typical parameters for a planar, single-gate (bulk or SOI) MOSFET. + /// Identical to `Default::default()` — planar/single-gate is this + /// model's baseline architecture. + pub fn planar() -> Self { + Self::default().with_gate_geometry(GateGeometry::SingleGate) + } + + /// Typical parameters for a double-gate FinFET: a thin fin (`d_ch` is + /// the fin width) with two active gates on the sidewalls (see + /// [`GateGeometry::DoubleGate`]; use + /// `.with_gate_geometry(GateGeometry::TriGate)` on the result if the + /// fin top is also gated) and a thin oxide, both narrower than the + /// planar preset for realistic electrostatic control. + pub fn fin_fet() -> Self { + Self { + d_ch: 8.0, + d_ox: 1.5, + ..Self::default() + } + .with_gate_geometry(GateGeometry::DoubleGate) + } + + /// Typical parameters for a gate-all-around nanoribbon/nanowire FET: + /// a narrow body (`d_ch` is the ribbon width/diameter) fully wrapped + /// by the gate ([`GateGeometry::GateAllAround`]), with a thin oxide. + pub fn nanoribbon() -> Self { + Self { + d_ch: 5.0, + d_ox: 1.0, + ..Self::default() + } + .with_gate_geometry(GateGeometry::GateAllAround) + } +} + /// A 1D ballistic-MOSFET device: geometry, electrostatics and derived /// transport quantities. #[derive(Debug, Clone)] @@ -133,7 +237,30 @@ impl Device { /// constructor: `lambda` is computed first, then (if /// `auto_size_contacts`) `l_ds` is overwritten from it before the grid /// is sized. - pub fn new(mut params: DeviceParams) -> Self { + /// + /// # Panics + /// + /// Panics if `params` fails [`Self::try_new`]'s validation — see + /// there for what that covers. Use `try_new` instead if you're + /// constructing a device from values you don't already trust (e.g. + /// user-supplied parameters). + pub fn new(params: DeviceParams) -> Self { + Self::try_new(params).unwrap_or_else(|e| panic!("{e}")) + } + + /// Fallible version of [`Self::new`]. Returns + /// `Err(NegForgeError::InvalidParameter)` if any of the + /// geometry/material parameters that the electrostatic solve divides + /// by (`geo`, `d_ch`, `d_ox`, `k_si`, `k_ox`, `a`) or the grid sizing + /// (`l_ch`, and `l_ds` when `auto_size_contacts` is `false`) are + /// non-positive or non-finite. Silently letting one of these through + /// would produce an infinite or NaN natural length and propagate + /// garbage through the rest of the solve instead of failing at the + /// point of misconfiguration — a real risk once `geo` is something a + /// caller sets explicitly (e.g. via [`GateGeometry`]) rather than only + /// ever the hardcoded default. + pub fn try_new(mut params: DeviceParams) -> crate::error::Result { + Self::validate_params(¶ms)?; let lambda = Self::compute_lambda(¶ms); if params.auto_size_contacts { params.l_ds = (lambda.floor()) * 15.0; @@ -160,7 +287,34 @@ impl Device { t_hop, }; device.init_vectors(); - device + Ok(device) + } + + 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(()) } fn compute_lambda(params: &DeviceParams) -> f64 { @@ -340,4 +494,22 @@ mod tests { "current should increase with gate bias: {i0} -> {i1}" ); } + + #[test] + #[should_panic(expected = "geo")] + fn rejects_zero_geo() { + Device::new(DeviceParams { + geo: 0.0, + ..Default::default() + }); + } + + #[test] + #[should_panic(expected = "d_ch")] + fn rejects_negative_d_ch() { + Device::new(DeviceParams { + d_ch: -1.0, + ..Default::default() + }); + } } diff --git a/crates/negforge-core/src/lib.rs b/crates/negforge-core/src/lib.rs index 2494e99..77cb4e3 100644 --- a/crates/negforge-core/src/lib.rs +++ b/crates/negforge-core/src/lib.rs @@ -14,7 +14,7 @@ pub mod selfconsistent; pub mod sweep; mod tridiag; -pub use device::{Device, DeviceParams}; +pub use device::{Device, DeviceParams, GateGeometry}; pub use error::{NegForgeError, Result}; pub use negf::{GreenFunctionAlgorithm, GreenFunctionResult}; pub use selfconsistent::{SelfConsistentOptions, SelfConsistentResult}; diff --git a/crates/negforge-core/tests/multigate_geometry.rs b/crates/negforge-core/tests/multigate_geometry.rs new file mode 100644 index 0000000..6232433 --- /dev/null +++ b/crates/negforge-core/tests/multigate_geometry.rs @@ -0,0 +1,143 @@ +//! Physical sanity checks for the planar/FinFET/nanoribbon presets — the +//! "make sure the solver works for these architectures" checks. +//! +//! Background: `lambda = sqrt(k_si/k_ox * d_ch * d_ox / geo)` (the +//! "generalized scale length" model) is how this 1D model distinguishes +//! device architectures — `geo` is the number of gates. See the +//! `device.rs` module docs. + +use negforge_core::selfconsistent::{solve_self_consistent, SelfConsistentOptions}; +use negforge_core::sweep::{subthreshold_swing, sweep_v_g}; +use negforge_core::{Device, DeviceParams, GateGeometry}; + +#[test] +fn presets_have_the_expected_gate_counts() { + assert_eq!(DeviceParams::planar().geo, 1.0); + assert_eq!(DeviceParams::fin_fet().geo, 2.0); + assert_eq!(DeviceParams::nanoribbon().geo, 4.0); +} + +#[test] +fn with_gate_geometry_only_changes_geo() { + let base = DeviceParams::planar(); + let tri_gate = base.with_gate_geometry(GateGeometry::TriGate); + assert_eq!(tri_gate.geo, 3.0); + // Everything else should be untouched. + assert_eq!(tri_gate.d_ch, base.d_ch); + assert_eq!(tri_gate.d_ox, base.d_ox); + assert_eq!(tri_gate.a, base.a); +} + +#[test] +fn more_gates_means_a_shorter_natural_length() { + // lambda ~ 1/sqrt(geo), so more gates -> tighter electrostatic + // control, for otherwise-comparable body/oxide thickness. + let planar = Device::new(DeviceParams::planar()); + let fin_fet = Device::new(DeviceParams::fin_fet()); + let nanoribbon = Device::new(DeviceParams::nanoribbon()); + + assert!( + planar.lambda > fin_fet.lambda, + "planar lambda ({}) should exceed FinFET lambda ({})", + planar.lambda, + fin_fet.lambda + ); + assert!( + fin_fet.lambda > nanoribbon.lambda, + "FinFET lambda ({}) should exceed nanoribbon lambda ({})", + fin_fet.lambda, + nanoribbon.lambda + ); +} + +#[test] +fn all_three_architectures_solve_without_producing_nan_or_panicking() { + for params in [ + DeviceParams::planar(), + DeviceParams::fin_fet(), + DeviceParams::nanoribbon(), + ] { + let mut dev = Device::new(DeviceParams { + v_ds: 0.3, + v_g: 0.2, + ..params + }); + dev.calc_potential(); + assert!(dev.psi_f.iter().all(|v| v.is_finite())); + let current = dev.calc_current(); + assert!(current.is_finite() && current >= 0.0); + } +} + +#[test] +fn all_three_architectures_converge_self_consistently() { + // Small devices (short l_ch, coarse grid) to keep the test fast; see + // geometry_comparison.rs for a realistic-scale comparison. + for params in [ + DeviceParams::planar(), + DeviceParams::fin_fet(), + DeviceParams::nanoribbon(), + ] { + let mut dev = Device::new(DeviceParams { + a: 1.0, + l_ch: 10.0, + l_ds: 15.0, + auto_size_contacts: false, + v_ds: 0.3, + v_g: 0.2, + d_e: 0.01, + ..params + }); + let opts = SelfConsistentOptions { + eta: 0.05, + ..Default::default() + }; + let result = solve_self_consistent(&mut dev, &opts) + .unwrap_or_else(|e| panic!("geo={} should converge: {e}", dev.params.geo)); + assert!(result.residual < opts.tolerance); + assert!(dev.psi_f.iter().all(|v| v.is_finite())); + } +} + +#[test] +fn more_gates_gives_better_subthreshold_swing_at_short_channel_length() { + // The textbook multi-gate result: at a channel length short relative + // to the planar device's natural length, planar suffers a + // short-channel-effect-degraded (larger) subthreshold swing, while + // FinFET (more gates) and especially gate-all-around nanoribbon (most + // gates) stay much closer to the ideal thermal limit + // (ln(10)*kT/e = 59.6 mV/decade at 300 K). + let swing_for = |params: DeviceParams| { + let dev = Device::new(DeviceParams { + l_ch: 10.0, + v_ds: 0.3, + ..params + }); + let points = sweep_v_g(&dev, 0.0, 0.4, 0.02, None).unwrap(); + subthreshold_swing(&points, 0.0, 0.4) + }; + + let s_planar = swing_for(DeviceParams::planar()); + let s_fin_fet = swing_for(DeviceParams::fin_fet()); + let s_nanoribbon = swing_for(DeviceParams::nanoribbon()); + + assert!( + s_planar > s_fin_fet, + "planar swing ({:.1} mV/dec) should be worse than FinFET ({:.1} mV/dec)", + s_planar * 1000.0, + s_fin_fet * 1000.0 + ); + assert!( + s_fin_fet > s_nanoribbon, + "FinFET swing ({:.1} mV/dec) should be worse than nanoribbon ({:.1} mV/dec)", + s_fin_fet * 1000.0, + s_nanoribbon * 1000.0 + ); + // Gate-all-around should be close to the ideal limit even at this + // short channel length. + assert!( + s_nanoribbon * 1000.0 < 70.0, + "nanoribbon swing ({:.1} mV/dec) should be close to the 59.6 mV/dec ideal limit", + s_nanoribbon * 1000.0 + ); +} diff --git a/crates/negforge-py/src/lib.rs b/crates/negforge-py/src/lib.rs index 613661a..1cc93c4 100644 --- a/crates/negforge-py/src/lib.rs +++ b/crates/negforge-py/src/lib.rs @@ -8,8 +8,16 @@ use pyo3::prelude::*; use negforge_core::{Device, DeviceParams, GreenFunctionAlgorithm, SelfConsistentOptions}; +/// Map a [`negforge_core::NegForgeError`] to the appropriate Python +/// exception type: `InvalidParameter` (bad user input, e.g. a +/// non-physical device geometry) becomes a `ValueError`, matching Python +/// convention; everything else (currently just `NotConverged`, a runtime +/// condition rather than a caller mistake) becomes a `RuntimeError`. fn to_py_err(e: negforge_core::NegForgeError) -> PyErr { - PyRuntimeError::new_err(e.to_string()) + match e { + negforge_core::NegForgeError::InvalidParameter(_) => PyValueError::new_err(e.to_string()), + negforge_core::NegForgeError::NotConverged { .. } => PyRuntimeError::new_err(e.to_string()), + } } /// Parse the Python-facing `algorithm` string ("recursive" or "dense") into @@ -59,7 +67,7 @@ impl PyDevice { e_fs: f64, t: f64, d_e: f64, - ) -> Self { + ) -> PyResult { let params = DeviceParams { a, e_f, @@ -81,9 +89,9 @@ impl PyDevice { d_e, m_eff: 0.9 * negforge_core::constants::M_E, }; - Self { - inner: Device::new(params), - } + Ok(Self { + inner: Device::try_new(params).map_err(to_py_err)?, + }) } fn calc_potential(&mut self) { diff --git a/notebooks/negforge_demo.ipynb b/notebooks/negforge_demo.ipynb index e89668c..a3a0665 100644 --- a/notebooks/negforge_demo.ipynb +++ b/notebooks/negforge_demo.ipynb @@ -281,6 +281,57 @@ " v_ds=FloatSlider(min=0.0, max=1.0, step=0.05, value=0.3),\n", ");" ] + }, + { + "cell_type": "markdown", + "id": "ab3f3498", + "metadata": {}, + "source": [ + "## 8. Planar, FinFET and nanoribbon devices\n", + "\n", + "`negforge.Device.planar()` / `.fin_fet()` / `.nanoribbon()` are presets for the three architectures this 1D model can represent, differing in the number of gates (`geo`) controlling the channel — see the README's \"Planar, FinFET and nanoribbon devices\" section for the physics. More gates means tighter electrostatic control (a smaller natural length), which shows up directly as a better (smaller) subthreshold swing at short channel lengths, closer to the 59.6 mV/decade ideal thermal limit at 300 K." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "029b759a", + "metadata": {}, + "outputs": [], + "source": [ + "architectures = {\n", + " \"planar\": negforge.Device.planar,\n", + " \"fin_fet\": negforge.Device.fin_fet,\n", + " \"nanoribbon\": negforge.Device.nanoribbon,\n", + "}\n", + "\n", + "fig, (ax_pot, ax_bar) = plt.subplots(1, 2, figsize=(12, 4.5))\n", + "swings = {}\n", + "for name, factory in architectures.items():\n", + " dev = factory(l_ch=10.0, v_ds=0.3)\n", + " dev.calc_potential()\n", + " ax_pot.plot(dev.positions_nm, dev.psi_f, label=f\"{name} ($\\\\lambda$={dev.screening_length:.2f} nm)\")\n", + "\n", + " curve = dev.sweep_v_g(0.0, 0.4, 0.02)\n", + " swings[name] = curve.subthreshold_swing(0.0, 0.4) * 1000\n", + "\n", + "ax_pot.set_xlabel(\"position (nm)\")\n", + "ax_pot.set_ylabel(r\"$\\Psi_f$ (eV)\")\n", + "ax_pot.set_title(\"Potential profile, l_ch = 10 nm (all architectures)\")\n", + "ax_pot.legend()\n", + "ax_pot.grid(alpha=0.3)\n", + "\n", + "ax_bar.bar(swings.keys(), swings.values())\n", + "ax_bar.axhline(59.6, color=\"k\", linestyle=\"--\", label=\"ideal (59.6 mV/decade)\")\n", + "ax_bar.set_ylabel(\"subthreshold swing (mV/decade)\")\n", + "ax_bar.set_title(\"Subthreshold swing, l_ch = 10 nm\")\n", + "ax_bar.legend()\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "for name, s in swings.items():\n", + " print(f\"{name:<10} S = {s:.1f} mV/decade\")" + ] } ], "metadata": { diff --git a/python/negforge/__init__.py b/python/negforge/__init__.py index e703163..ee5151d 100644 --- a/python/negforge/__init__.py +++ b/python/negforge/__init__.py @@ -64,6 +64,40 @@ def __init__(self, **kwargs): def __repr__(self): return f"Device(n={self.n}, a={self.a} nm, screening_length={self.screening_length:.3f} nm)" + # -- architecture presets ------------------------------------------------ + # + # `geo` (number of gates) is what actually distinguishes a planar, + # FinFET, or gate-all-around/nanoribbon device in this model's natural- + # length electrostatics (lambda ~ 1/sqrt(geo) -- more gates means + # tighter electrostatic control). These mirror the presets in + # `negforge_core::DeviceParams` (`planar`/`fin_fet`/`nanoribbon`); see + # the top-level README's "Planar, FinFET and nanoribbon devices" + # section for the physics and for why this ordering (planar worst, + # nanoribbon best short-channel behavior) is expected. + + @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) + + @classmethod + def fin_fet(cls, **overrides) -> "Device": + """A double-gate FinFET: a thin fin (`d_ch` = fin width) gated on + both sidewalls, with a thinner oxide than the planar preset. Pass + `geo=3.0` to also gate the fin top (tri-gate).""" + params = {"geo": 2.0, "d_ch": 8.0, "d_ox": 1.5} + params.update(overrides) + return cls(**params) + + @classmethod + def nanoribbon(cls, **overrides) -> "Device": + """A gate-all-around nanoribbon/nanowire FET: a narrow body + (`d_ch` = ribbon width/diameter) fully wrapped by the gate.""" + params = {"geo": 4.0, "d_ch": 5.0, "d_ox": 1.0} + params.update(overrides) + return cls(**params) + # -- electrostatics ---------------------------------------------------- def calc_potential(self) -> "Device":