From 5d3e2c3c257b601d0ddabcfb65991eb8100d7068 Mon Sep 17 00:00:00 2001 From: Wenjun Ke Date: Tue, 28 Jul 2026 10:30:29 +0800 Subject: [PATCH] Add magnon-dispersion-bec task YIG dipole-exchange spin-wave dispersion parameter extraction plus pumped magnon-gas thermodynamics with a BEC threshold. Self-contained numerical problem; no bundled input data. Registers the task in registry.json and validate_repository.py. Co-Authored-By: Claude Opus 5 --- .github/scripts/validate_repository.py | 1 + registry.json | 5 + .../environment/Dockerfile | 25 +++ .../skills/magnon-gas-thermodynamics/SKILL.md | 66 ++++++ .../references/model.md | 40 ++++ .../oracle/magnon_physics.py | 207 ++++++++++++++++++ .../oracle/provenance/manifest.json | 46 ++++ tasks/magnon-dispersion-bec/oracle/solve.py | 68 ++++++ tasks/magnon-dispersion-bec/oracle/solve.sh | 4 + tasks/magnon-dispersion-bec/task.md | 105 +++++++++ .../verifier/magnon_physics.py | 207 ++++++++++++++++++ tasks/magnon-dispersion-bec/verifier/test.sh | 10 + .../verifier/test_outputs.py | 150 +++++++++++++ 13 files changed, 934 insertions(+) create mode 100644 tasks/magnon-dispersion-bec/environment/Dockerfile create mode 100644 tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/SKILL.md create mode 100644 tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/references/model.md create mode 100644 tasks/magnon-dispersion-bec/oracle/magnon_physics.py create mode 100644 tasks/magnon-dispersion-bec/oracle/provenance/manifest.json create mode 100644 tasks/magnon-dispersion-bec/oracle/solve.py create mode 100755 tasks/magnon-dispersion-bec/oracle/solve.sh create mode 100644 tasks/magnon-dispersion-bec/task.md create mode 100644 tasks/magnon-dispersion-bec/verifier/magnon_physics.py create mode 100755 tasks/magnon-dispersion-bec/verifier/test.sh create mode 100644 tasks/magnon-dispersion-bec/verifier/test_outputs.py diff --git a/.github/scripts/validate_repository.py b/.github/scripts/validate_repository.py index 2615923..60c02b5 100644 --- a/.github/scripts/validate_repository.py +++ b/.github/scripts/validate_repository.py @@ -6,6 +6,7 @@ ROOT = Path(__file__).resolve().parents[2] EXPECTED_TASKS = ( "heterodyne-shot-noise-analysis", + "magnon-dispersion-bec", "surface-ion-trap-shuttling", "trapped-ions-heating-rate", ) diff --git a/registry.json b/registry.json index 6854b7a..308d5a2 100644 --- a/registry.json +++ b/registry.json @@ -12,6 +12,11 @@ "path": "tasks/heterodyne-shot-noise-analysis", "digest": "sha256:497468dab7ed79dca0f5b5768eb5b854b0fdd420247952f49c741f63d6ae27f2" }, + { + "name": "magnon-dispersion-bec", + "path": "tasks/magnon-dispersion-bec", + "digest": "sha256:9ff077258443093a1c471d49abfd3685b53494133400b5b4ff4f0c579f661938" + }, { "name": "surface-ion-trap-shuttling", "path": "tasks/surface-ion-trap-shuttling", diff --git a/tasks/magnon-dispersion-bec/environment/Dockerfile b/tasks/magnon-dispersion-bec/environment/Dockerfile new file mode 100644 index 0000000..7786e1b --- /dev/null +++ b/tasks/magnon-dispersion-bec/environment/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + MPLBACKEND=Agg + +RUN pip install --no-cache-dir \ + numpy==2.2.6 \ + scipy==1.15.3 \ + matplotlib==3.10.3 \ + pytest==8.4.1 \ + pytest-json-ctrf==0.3.5 + +WORKDIR /app + +RUN mkdir -p \ + /app \ + /home/agent/.claude \ + /home/agent/.codex \ + /home/agent/.opencode \ + /home/agent/.agents && \ + chmod -R 777 /home/agent && \ + chmod 777 /root diff --git a/tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/SKILL.md b/tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/SKILL.md new file mode 100644 index 0000000..327dabf --- /dev/null +++ b/tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/SKILL.md @@ -0,0 +1,66 @@ +--- +name: magnon-gas-thermodynamics +description: Reusable guidance for YIG-film spin-wave calculations and equilibrium or pumped magnon-gas thermodynamics. +--- + +# YIG spin waves and magnon gases + +Use this guide when a problem combines finite-thickness ferromagnetic-film +spectra with a thermal or pumped magnon gas. Keep the field, wavevector, +thickness, frequency, and energy units consistent before doing any fitting or +integration. + +## Film spectra + +- Use the full finite-thickness dipole-exchange dispersion for an + in-plane-magnetized film. With `theta` measured from the magnetization, + `F(x) = (1 - exp(-x))/x` and + + `f(k, theta) = gamma * sqrt((H + D*k^2 + 4piM*(1 - F(k*d))*sin(theta)^2) * (H + D*k^2 + 4piM*F(k*d)))`. + + Take `F(0) = 1`, keep the propagation angle, and use the units supplied by + the problem. An exchange-only or bulk approximation can give the wrong + branch and minimum. +- At zero wavevector, use the Kittel relation to infer the saturation + magnetization when an FMR frequency is supplied. +- Infer exchange stiffness from an observed finite-wavevector minimum by + minimizing the backward-volume branch while fitting the stiffness. Recompute + the minimum after changing only the thickness when comparing films of the + same material. +- Evaluate surface or Damon-Eshbach predictions with the same material + parameters and full form factor. Use a bounded minimizer and check that the + result is stable when the wavevector grid or bracket changes. + +## Thermal magnons + +- Decide whether the thermal cloud is a 3D bulk gas or a lower-dimensional + film gas from the physical wavelength and geometry. For an exchange-dominated + 3D cloud, use the isotropic dispersion + `epsilon(k) = h * gamma * (H + D*k^2)`. +- Compute number and energy from the Bose-Einstein occupation with a + three-dimensional density-of-states factor. Convert frequency to joules with + `h`, and keep the integration wavevector in the units used by `D`. +- Check the spectral gap at the lowest energy, the density scaling with + temperature, quadrature convergence, and the high-wavevector cutoff. + +## Pumped quasi-equilibrium + +- Rapid thermalization with conserved magnon number and energy gives two + conservation equations for the pumped temperature and chemical potential. + Include the injected energy at the pump frequency. +- Below condensation, solve for `(T, mu)` while enforcing `mu` below the + spectral minimum. The critical injected density is the increase needed to + reach that minimum at the reference temperature. +- Above threshold, pin `mu` at the minimum and solve for the thermal + temperature plus condensate density. Count condensate magnons at the minimum + energy when enforcing energy conservation, then divide by total density for + the condensate fraction. +- If one configuration feeds a fitted material parameter into another, pass + the computed value through rather than substituting a nominal material value. + +## Numerical checks + +Use SI constants for energies, document any unit conversions, bracket roots +where possible, and verify both number and energy conservation from the final +state. Keep scripts and intermediate calculations general; do not hardcode +expected outputs or task-specific field names. diff --git a/tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/references/model.md b/tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/references/model.md new file mode 100644 index 0000000..4f6e008 --- /dev/null +++ b/tasks/magnon-dispersion-bec/environment/skills/magnon-gas-thermodynamics/references/model.md @@ -0,0 +1,40 @@ +# Units and worked checks + +## Constants +- h = 6.62607015e-34 J s, kB = 1.380649e-23 J/K +- gamma = 2.8e-3 GHz/Gauss = 2.8e6 Hz/Gauss (YIG) + +## Part A units +- H and 4piM in Gauss (Oe); D in Oe cm^2 with k in cm^-1, so `D k^2` is in Gauss. +- `omega(k,theta)` above is in GHz. f_FMR, f_min in GHz. +- The dipolar form factor F(k) is dimensionless; `k d` uses d in cm. +- Sanity: with H around a few hundred Gauss and 4piM ~ 1.8e3 Gauss, f_FMR is a + few GHz; the backward-volume minimum sits a few hundred MHz below f_FMR at + k0 ~ 1e5 cm^-1. + +## Part B units +- Bulk magnon energy `eps(k) = h * gamma * (H + D k^2) * 1e9` in Joules + (the `1e9` converts GHz -> Hz before multiplying by h). +- Number density `n = (1/(2 pi^2)) int_0^inf k^2 nBE dk` with k in cm^-1 gives + cm^-3 directly (the 3D density of states `d^3k/(2 pi)^3` integrated over solid + angle gives `k^2/(2 pi^2) dk`). +- Energy density e in J/cm^3. +- Bose factor uses `expm1` for numerical stability: `1/np.expm1(x)`. + +## Worked check (arbitrary parameters, not the task values) +For H = 600 Gauss, D = 2e-9, T = 300 K, mu = 0: +- eps(0)/kB = h*gamma*H*1e9/kB should be ~ 81 mK. +- the number integral is dominated by k ~ 1e7 cm^-1 (where eps ~ kB T) and the + density comes out of order 1e21 cm^-3, following n(T) proportional to T^1.5 + (so the density-vs-temperature exponent is ~1.5, not ~1). + +## Supercritical / condensed branch (units) +- With `mu` pinned at `eps_min`, the thermal-cloud integrals `n(T, eps_min)` and + `e(T, eps_min)` are evaluated exactly as above but at `mu = eps_min` (use + `mu = eps_min (1 - tiny)` to avoid the k=0 singularity of the Bose factor). +- The condensate density `n_c` (cm^-3) is a number, sitting at energy `eps_min` + per magnon; it carries energy `n_c eps_min` (J/cm^3). +- Condensate fraction is dimensionless: `n_c / (n(T0,0) + delta_N)`. + +Use these to validate your dispersion and integration before computing the task +values. Do NOT hardcode any result; derive everything from the stated inputs. diff --git a/tasks/magnon-dispersion-bec/oracle/magnon_physics.py b/tasks/magnon-dispersion-bec/oracle/magnon_physics.py new file mode 100644 index 0000000..5bb54ea --- /dev/null +++ b/tasks/magnon-dispersion-bec/oracle/magnon_physics.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Physics core for the YIG magnon dispersion + pumped-magnon-gas task (oracle copy). + +Part A -- dipole-exchange spin-wave dispersion of a magnetized YIG film +(Rezende, PRB 79, 174411 (2009)); extract the saturation magnetization 4piM +(Kittel) and the exchange stiffness D (from the spectral minimum f_min), then +predict the surface (Damon-Eshbach) branch and a second film's spectral minimum. + +Part B -- thermodynamics of the parametrically pumped magnon gas following +Demokritov et al., Nature 443, 430 (2006): the exchange-dominated 3D bulk +thermal magnon gas (Bloch T^3/2 law), the chemical potential / temperature of +the pumped quasi-equilibrium gas from magnon-number and energy conservation, +the critical pumped density for Bose-Einstein condensation, and the condensed +branch (condensate fraction) of a supercritically pumped gas. +""" + +from __future__ import annotations + +import numpy as np +from scipy.integrate import quad +from scipy.optimize import brentq, fsolve + +H_PLANCK = 6.62607015e-34 # J s +KB = 1.380649e-23 # J / K +GAMMA = 2.8e-3 # GHz / Gauss (gyromagnetic ratio for YIG) + +# ============================ PART A ============================ +# NV3 experimental inputs (film 1) +NV3 = {"H": 330.0, "f_FMR": 2.395, "f_min": 1.913, "d": 0.206e-4} # d in cm (0.206 um) +D2_FILM = 0.5e-4 # second film thickness (cm), same material, same field H +K_S = 8.0e4 # surface-branch (theta = pi/2) probe wavevector (cm^-1) for film 1 + + +def _F(k, d): + k = np.asarray(k, float) + return np.where(k != 0, (1 - np.exp(-k * d)) / (k * d), 1.0) + + +def film_dispersion_GHz(k, theta, H, fourpiM, D, d): + """Rezende dipole-exchange film magnon frequency (GHz). k in cm^-1.""" + a = H + D * k**2 + fourpiM * (1 - _F(k, d)) * np.sin(theta) ** 2 + b = H + D * k**2 + fourpiM * _F(k, d) + return GAMMA * np.sqrt(a * b) + + +def extract_fourpiM(f_FMR, H): + """Kittel: f_FMR = gamma sqrt(H (H + 4piM)) -> 4piM.""" + return f_FMR**2 / (GAMMA**2 * H) - H + + +def film_fmin(H, fourpiM, D, d, n=400000): + ks = np.linspace(1.0, 3e5, n) + fk = film_dispersion_GHz(ks, 0.0, H, fourpiM, D, d) + i = int(np.argmin(fk)) + return float(fk[i]), float(ks[i]) + + +def extract_D(f_min_meas, H, fourpiM, d): + """Fit D so the backward-volume (theta=0) spectral minimum equals f_min.""" + return brentq(lambda D: film_fmin(H, fourpiM, D, d)[0] - f_min_meas, 1e-10, 3e-8) + + +def part_A(): + p = NV3 + fourpiM = extract_fourpiM(p["f_FMR"], p["H"]) + D = extract_D(p["f_min"], p["H"], fourpiM, p["d"]) + fmin, kmin = film_fmin(p["H"], fourpiM, D, p["d"]) + # surface (Damon-Eshbach, theta = pi/2) branch of film 1 at K_S + f_DE = float(film_dispersion_GHz(K_S, np.pi / 2, p["H"], fourpiM, D, p["d"])) + # second film (same material -> same 4piM, D) spectral minimum at thickness D2_FILM + fmin2, kmin2 = film_fmin(p["H"], fourpiM, D, D2_FILM) + return { + "fourpiM_G": fourpiM, + "D_Oecm2": D, + "f_min_GHz": fmin, + "k_min_percm": kmin, + "f_DE_GHz": f_DE, + "fmin2_GHz": fmin2, + "kmin2_percm": kmin2, + } + + +# ============================ PART B ============================ +# Config B1: Nature-06 YIG system + sub-critical pumping +NB = {"H": 700.0, "D": 2e-9} # Gauss, Oe cm^2 +T0 = 300.0 # K +NU_P = 4.0e9 # pumped magnon frequency (Hz) +DELTA_N = 5e18 # pumped magnon density (cm^-3) + +# Config B2: a second YIG system, supercritically pumped (condenses) +NB2 = {"H": 500.0, "D": 2e-9} +T0_2 = 300.0 +NU_P2 = 4.0e9 +DELTA_N2 = 1.5e20 # cm^-3 (chosen well above the B2 critical density) + +# Config B3: interlocked system that uses the exchange stiffness extracted in Part A +NB3 = {"H": 400.0} # D supplied from Part A at runtime +T0_3 = 300.0 +DELTA_N3 = 2e18 # cm^-3 + + +def _eps(k, H, D): + """3D bulk exchange magnon energy (J). k in cm^-1.""" + return H_PLANCK * GAMMA * (H + D * k**2) * 1e9 + + +def bulk_density(T, mu, H, D): + """3D bulk thermal magnon density (cm^-3): n = (1/2pi^2) int k^2 nBE dk.""" + val, _ = quad(lambda k: k**2 / np.expm1((_eps(k, H, D) - mu) / (KB * T)), 0, 3e8, limit=600) + return val / (2 * np.pi**2) + + +def bulk_energy(T, mu, H, D): + val, _ = quad(lambda k: k**2 * _eps(k, H, D) / np.expm1((_eps(k, H, D) - mu) / (KB * T)), 0, 3e8, limit=600) + return val / (2 * np.pi**2) + + +def _subcritical_solve(H, D, T0_cfg, nu_p, delta_n, n0, e0): + """Pumped sub-critical quasi-equilibrium: conserve number and energy -> (T, mu).""" + nT = n0 + delta_n + eT = e0 + H_PLANCK * nu_p * delta_n + eps_min = _eps(0.0, H, D) + sol = fsolve( + lambda v: [(bulk_density(v[0], v[1], H, D) - nT) / nT, (bulk_energy(v[0], v[1], H, D) - eT) / eT], + [T0_cfg, eps_min * 0.3], + full_output=True, + ) + (T_p, mu_p), ier = sol[0], sol[2] + return T_p, mu_p, bool(ier == 1) + + +def _condensed_solve(H, D, T0_cfg, nu_p, delta_n, n0, e0): + """Pumped supercritical branch: mu pinned at eps_min, conserve number+energy + for (T, condensate density n_c). Returns (T_p, condensate_fraction).""" + eps_min = _eps(0.0, H, D) + mu_pin = eps_min * (1 - 1e-12) + n_tot = n0 + delta_n + e_tot = e0 + H_PLANCK * nu_p * delta_n + + def f_T(T): + n_th = bulk_density(T, mu_pin, H, D) + return bulk_energy(T, mu_pin, H, D) + (n_tot - n_th) * eps_min - e_tot + + T_p = brentq(f_T, 20.0, 3.0 * T0_cfg) + n_c = n_tot - bulk_density(T_p, mu_pin, H, D) + return T_p, n_c / n_tot + + +def part_B(D_from_A=None): + # ---- Config B1 (sub-critical) ---- + H, D = NB["H"], NB["D"] + eps_min = _eps(0.0, H, D) # k=0 gap = h gamma H + n0 = bulk_density(T0, 0.0, H, D) + e0 = bulk_energy(T0, 0.0, H, D) + n_crit = bulk_density(T0, eps_min * (1 - 1e-9), H, D) # density at mu -> eps_min + dN_c = n_crit - n0 + T_p, mu_p, conv1 = _subcritical_solve(H, D, T0, NU_P, DELTA_N, n0, e0) + # Bloch exponent of the gapless 3D gas: n0(T) ~ T^p near T0 + p_bloch = float(np.log(bulk_density(2 * T0, 0.0, H, D) / n0) / np.log(2.0)) + + # ---- Config B2 (supercritical -> condensate) ---- + H2, D2 = NB2["H"], NB2["D"] + eps_min2 = _eps(0.0, H2, D2) + n0_2 = bulk_density(T0_2, 0.0, H2, D2) + e0_2 = bulk_energy(T0_2, 0.0, H2, D2) + dN_c2 = bulk_density(T0_2, eps_min2 * (1 - 1e-9), H2, D2) - n0_2 + bec2 = bool(DELTA_N2 > dN_c2) + T_p2, ncfrac2 = _condensed_solve(H2, D2, T0_2, NU_P2, DELTA_N2, n0_2, e0_2) + + # ---- Config B3 (A->B interlock: uses D extracted in Part A) ---- + D3 = D_from_A if D_from_A is not None else part_A()["D_Oecm2"] + H3 = NB3["H"] + eps_min3 = _eps(0.0, H3, D3) + n0_3 = bulk_density(T0_3, 0.0, H3, D3) + dN_c3 = bulk_density(T0_3, eps_min3 * (1 - 1e-9), H3, D3) - n0_3 + bec3 = bool(DELTA_N3 > dN_c3) + + return { + # config B1 + "eps_min_mK": eps_min / KB * 1e3, + "n0_percm3": n0, + "e0_Jpercm3": e0, + "p_bloch": p_bloch, + "dN_c_percm3": dN_c, + "mu_p_mK": mu_p / KB * 1e3, + "T_p_K": T_p, + "bec": bool(DELTA_N > dN_c), + "converged": conv1, + # config B2 (supercritical) + "eps_min2_mK": eps_min2 / KB * 1e3, + "dN_c2_percm3": dN_c2, + "bec2": bec2, + "ncfrac2": ncfrac2, + "T_p2_K": T_p2, + # config B3 (interlock) + "n0_3_percm3": n0_3, + "dN_c3_percm3": dN_c3, + "bec3": bec3, + } + + +if __name__ == "__main__": + import json + + A = part_A() + B = part_B(D_from_A=A["D_Oecm2"]) + print(json.dumps({"A": A, "B": B}, indent=2)) diff --git a/tasks/magnon-dispersion-bec/oracle/provenance/manifest.json b/tasks/magnon-dispersion-bec/oracle/provenance/manifest.json new file mode 100644 index 0000000..82c8eda --- /dev/null +++ b/tasks/magnon-dispersion-bec/oracle/provenance/manifest.json @@ -0,0 +1,46 @@ +{ + "schema_version": "frontierphysics-provenance-v1", + "task_id": "magnon-dispersion-bec", + "visibility": "reviewer_only", + "one_line": "YIG dipole-exchange spin-wave dispersion parameter extraction and pumped magnon-gas thermodynamics with Bose-Einstein condensation threshold.", + "source_model": { + "part_a": "In-plane magnetized YIG-film dipole-exchange spin-wave dispersion, using the measured ferromagnetic resonance and spectral minimum to infer 4piM, exchange stiffness, and k_min, then forward-predicting the surface (theta=pi/2) branch and a second film's spectral minimum from the same material parameters.", + "part_b": "Three-dimensional exchange-dominated magnon gas at room temperature with conserved magnon number and total energy under parametric pumping, across three coupled configurations: a sub-critical pumped gas (chemical potential, temperature, BEC threshold, T^3/2 density scaling), a supercritically pumped gas that condenses (condensate fraction on the mu=eps_min branch), and a configuration whose exchange stiffness is the value extracted in Part A." + }, + "primary_references": [ + { + "citation": "D. D. Stancil and A. Prabhakar, Spin Waves: Theory and Applications, Springer, 2009", + "used_for": "Magnetostatic and exchange spin-wave dispersion conventions." + }, + { + "citation": "A. V. Chumak et al., Magnon spintronics, Nature Physics 11, 453-461 (2015)", + "doi": "10.1038/nphys3347", + "used_for": "YIG magnonics context and material scale." + }, + { + "citation": "S. O. Demokritov et al., Bose-Einstein condensation of quasi-equilibrium magnons at room temperature under pumping, Nature 443, 430-433 (2006)", + "doi": "10.1038/nature05117", + "used_for": "Pumped quasi-equilibrium magnon gas and BEC threshold context." + }, + { + "citation": "A. A. Serga, A. V. Chumak, and B. Hillebrands, YIG magnonics, Journal of Physics D 43, 264002 (2010)", + "doi": "10.1088/0022-3727/43/26/264002", + "used_for": "YIG spin-wave and magnon-gas phenomenology." + } + ], + "input_data": { + "type": "self-contained numerical problem statement", + "agent_visible": "All numerical inputs are in task.md.", + "external_files": "None" + }, + "oracle_method": { + "route": "Solve the in-plane film dispersion equations for 4piM, D, and k_min (and forward-evaluate the surface branch and second-film minimum), then evaluate the exchange-dominated magnon-gas number and energy integrals and solve number/energy conservation for the pumped temperature and chemical potential (sub-critical branch), the mu=eps_min condensed branch and its condensate fraction (supercritical system), and an interlocked configuration that consumes the Part-A exchange stiffness.", + "implementation": "oracle/magnon_physics.py and oracle/solve.py", + "derives_outputs": true + }, + "verifier_method": { + "route": "Independently imports verifier/magnon_physics.py to recompute the same physical quantities from task constants and compares with tolerance bands.", + "hidden_answer_in_environment": false + }, + "license": "Task-authored code and self-contained numerical inputs are contributed under the repository license." +} diff --git a/tasks/magnon-dispersion-bec/oracle/solve.py b/tasks/magnon-dispersion-bec/oracle/solve.py new file mode 100644 index 0000000..91caf35 --- /dev/null +++ b/tasks/magnon-dispersion-bec/oracle/solve.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Oracle for the YIG magnon dispersion + pumped-magnon-gas task.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import magnon_physics as mp + +ROOT = Path("/root") + + +def main(): + A = mp.part_A() + B = mp.part_B(D_from_A=A["D_Oecm2"]) + ROOT.mkdir(parents=True, exist_ok=True) + + md = f"""# YIG magnon dispersion and pumped magnon gas + +## Part A -- NV3 dipole-exchange dispersion, material parameters, surface branch, second film +## Part B -- pumped magnon-gas thermodynamics (three systems) + +``` +RESULT +fourpiM_G = {A["fourpiM_G"]:.6g} +D_Oecm2 = {A["D_Oecm2"]:.6g} +k_min_percm = {A["k_min_percm"]:.6g} +f_DE_GHz = {A["f_DE_GHz"]:.6g} +fmin2_GHz = {A["fmin2_GHz"]:.6g} +kmin2_percm = {A["kmin2_percm"]:.6g} +eps_min_mK = {B["eps_min_mK"]:.6g} +n0_percm3 = {B["n0_percm3"]:.6g} +e0_Jpercm3 = {B["e0_Jpercm3"]:.6g} +p_bloch = {B["p_bloch"]:.6g} +dN_c_percm3 = {B["dN_c_percm3"]:.6g} +mu_p_mK = {B["mu_p_mK"]:.6g} +T_p_K = {B["T_p_K"]:.6g} +bec = {int(B["bec"])} +eps_min2_mK = {B["eps_min2_mK"]:.6g} +dN_c2_percm3 = {B["dN_c2_percm3"]:.6g} +bec2 = {int(B["bec2"])} +ncfrac2 = {B["ncfrac2"]:.6g} +T_p2_K = {B["T_p2_K"]:.6g} +n0_3_percm3 = {B["n0_3_percm3"]:.6g} +dN_c3_percm3 = {B["dN_c3_percm3"]:.6g} +bec3 = {int(B["bec3"])} +END +``` + +Part A: extracted 4piM = {A["fourpiM_G"]:.1f} G, D = {A["D_Oecm2"]:.3e} Oe cm^2 +(spectral minimum {A["f_min_GHz"]:.4f} GHz at k = {A["k_min_percm"]:.3e} cm^-1); +surface branch {A["f_DE_GHz"]:.4f} GHz; second film minimum {A["fmin2_GHz"]:.4f} GHz +at k = {A["kmin2_percm"]:.3e} cm^-1. +Part B: thermal magnon density {B["n0_percm3"]:.3e} cm^-3 (Bloch exponent +{B["p_bloch"]:.3f}); config-1 pumped gas reaches mu/kB = {B["mu_p_mK"]:.2f} mK at +T = {B["T_p_K"]:.1f} K (sub-critical, dN_c = {B["dN_c_percm3"]:.3e} cm^-3). +Config-2 supercritically pumped gas condenses (dN_c2 = {B["dN_c2_percm3"]:.3e} +cm^-3, condensate fraction {B["ncfrac2"]:.3f}). Config-3 (D from Part A) has +n0 = {B["n0_3_percm3"]:.3e} cm^-3, dN_c3 = {B["dN_c3_percm3"]:.3e} cm^-3. +""" + (ROOT / "result.md").write_text(md) + (ROOT / "oracle_diagnostics.json").write_text(json.dumps({"A": A, "B": B}, indent=2, sort_keys=True)) + print("oracle:", json.dumps({"A": A, "B": B}, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/tasks/magnon-dispersion-bec/oracle/solve.sh b/tasks/magnon-dispersion-bec/oracle/solve.sh new file mode 100755 index 0000000..8daac8b --- /dev/null +++ b/tasks/magnon-dispersion-bec/oracle/solve.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -euo pipefail +cd /oracle +python3 /oracle/solve.py diff --git a/tasks/magnon-dispersion-bec/task.md b/tasks/magnon-dispersion-bec/task.md new file mode 100644 index 0000000..e62b016 --- /dev/null +++ b/tasks/magnon-dispersion-bec/task.md @@ -0,0 +1,105 @@ +--- +schema_version: '1.3' +metadata: + author_name: Wenjun Ke + author_email: wenjunke@caltech.edu + difficulty: hard + difficulty_explanation: Requires coupled dipole-exchange parameter extraction, forward dispersion predictions, and number- and energy-conserving thermodynamics for subcritical and condensed magnon gases. + category: natural-science + subcategory: magnonics + category_confidence: high + task_type: + - calculation + - analysis + modality: + - document + interface: + - terminal + - python + skill_type: + - domain-procedure + - mathematical-method + tags: + - magnonics + - spin-waves + - yttrium-iron-garnet + - dispersion-relation + - magnon-bose-einstein-condensation + - condensed-matter +verifier: + type: test-script + timeout_sec: 900.0 + service: main + pytest_plugins: + - ctrf + hardening: + cleanup_conftests: true +agent: + timeout_sec: 3600.0 +environment: + network_mode: public + build_timeout_sec: 900.0 + os: linux + cpus: 2 + memory_mb: 4096 + storage_mb: 10240 + gpus: 0 +--- + +Use the standard in-plane YIG-film spin-wave model and the 3D +exchange-dominated magnon-gas model. Take +`gamma = 2.8e-3 GHz/Gauss`; use SI values for `h` and `kB`. + +For film 1, use `H = 330` Gauss, `f_FMR = 2.395` GHz, +`f_min = 1.913` GHz, and `d = 0.206` micrometre to find `4piM` (Gauss), +`D` (Oe cm^2), and `k_min` (cm^-1). With those same material parameters, +find `f_DE` (GHz) for the perpendicular-to-magnetization branch at +`k_s = 8e4` cm^-1, and the minimum frequency `fmin2` (GHz) and wavevector +`kmin2` (cm^-1) of a same-material film with `d2 = 0.5` micrometre. + +At `T0 = 300` K, calculate the following pumped-gas cases. Treat pumping as +rapid thermalization that conserves magnon number and total energy. + +- System 1: `H = 700` Gauss, `D = 2e-9` Oe cm^2, + `delta_N = 5e18` cm^-3 at `nu_p = 4` GHz. Report the equilibrium + `n0` (cm^-3), `e0` (J/cm^3), `eps_min/kB` (mK), density exponent + `p_bloch` measured between `T0` and `2*T0`, pumped `mu_p/kB` (mK), + `T_p` (K), critical `dN_c` (cm^-3), and `bec` (1 or 0). +- System 2: `H = 500` Gauss, `D = 2e-9` Oe cm^2, + `delta_N2 = 1.5e20` cm^-3 at `nu_p = 4` GHz. Report + `eps_min2/kB` (mK), `dN_c2` (cm^-3), `bec2`, condensate fraction + `ncfrac2`, and `T_p2` (K). If it does not condense, use `ncfrac2 = 0` + and report its pumped-gas temperature. +- System 3: `H = 400` Gauss, `D` equal to the value extracted for film 1, + and `delta_N3 = 2e18` cm^-3. Report `n0_3` (cm^-3), `dN_c3` (cm^-3), + and `bec3` (1 or 0). + +Write `/root/result.md` with a fenced block in exactly this format +(numbers only; use the stated units): + +``` +RESULT +fourpiM_G = <4piM, Gauss> +D_Oecm2 = +k_min_percm = +f_DE_GHz = +fmin2_GHz = +kmin2_percm = +eps_min_mK = +n0_percm3 = +e0_Jpercm3 = +p_bloch = +dN_c_percm3 = +mu_p_mK = +T_p_K = +bec = <1 if system-1 delta_N >= dN_c else 0> +eps_min2_mK = +dN_c2_percm3 = +bec2 = <1 if system-2 condenses else 0> +ncfrac2 = +T_p2_K = +n0_3_percm3 = +dN_c3_percm3 = +bec3 = <1 if system-3 condenses else 0> +END +``` diff --git a/tasks/magnon-dispersion-bec/verifier/magnon_physics.py b/tasks/magnon-dispersion-bec/verifier/magnon_physics.py new file mode 100644 index 0000000..e7b644b --- /dev/null +++ b/tasks/magnon-dispersion-bec/verifier/magnon_physics.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python3 +"""Physics core for the YIG magnon dispersion + pumped-magnon-gas task (verifier copy). + +Part A -- dipole-exchange spin-wave dispersion of a magnetized YIG film +(Rezende, PRB 79, 174411 (2009)); extract the saturation magnetization 4piM +(Kittel) and the exchange stiffness D (from the spectral minimum f_min), then +predict the surface (Damon-Eshbach) branch and a second film's spectral minimum. + +Part B -- thermodynamics of the parametrically pumped magnon gas following +Demokritov et al., Nature 443, 430 (2006): the exchange-dominated 3D bulk +thermal magnon gas (Bloch T^3/2 law), the chemical potential / temperature of +the pumped quasi-equilibrium gas from magnon-number and energy conservation, +the critical pumped density for Bose-Einstein condensation, and the condensed +branch (condensate fraction) of a supercritically pumped gas. +""" + +from __future__ import annotations + +import numpy as np +from scipy.integrate import quad +from scipy.optimize import brentq, fsolve + +H_PLANCK = 6.62607015e-34 # J s +KB = 1.380649e-23 # J / K +GAMMA = 2.8e-3 # GHz / Gauss (gyromagnetic ratio for YIG) + +# ============================ PART A ============================ +# NV3 experimental inputs (film 1) +NV3 = {"H": 330.0, "f_FMR": 2.395, "f_min": 1.913, "d": 0.206e-4} # d in cm (0.206 um) +D2_FILM = 0.5e-4 # second film thickness (cm), same material, same field H +K_S = 8.0e4 # surface-branch (theta = pi/2) probe wavevector (cm^-1) for film 1 + + +def _F(k, d): + k = np.asarray(k, float) + return np.where(k != 0, (1 - np.exp(-k * d)) / (k * d), 1.0) + + +def film_dispersion_GHz(k, theta, H, fourpiM, D, d): + """Rezende dipole-exchange film magnon frequency (GHz). k in cm^-1.""" + a = H + D * k**2 + fourpiM * (1 - _F(k, d)) * np.sin(theta) ** 2 + b = H + D * k**2 + fourpiM * _F(k, d) + return GAMMA * np.sqrt(a * b) + + +def extract_fourpiM(f_FMR, H): + """Kittel: f_FMR = gamma sqrt(H (H + 4piM)) -> 4piM.""" + return f_FMR**2 / (GAMMA**2 * H) - H + + +def film_fmin(H, fourpiM, D, d, n=400000): + ks = np.linspace(1.0, 3e5, n) + fk = film_dispersion_GHz(ks, 0.0, H, fourpiM, D, d) + i = int(np.argmin(fk)) + return float(fk[i]), float(ks[i]) + + +def extract_D(f_min_meas, H, fourpiM, d): + """Fit D so the backward-volume (theta=0) spectral minimum equals f_min.""" + return brentq(lambda D: film_fmin(H, fourpiM, D, d)[0] - f_min_meas, 1e-10, 3e-8) + + +def part_A(): + p = NV3 + fourpiM = extract_fourpiM(p["f_FMR"], p["H"]) + D = extract_D(p["f_min"], p["H"], fourpiM, p["d"]) + fmin, kmin = film_fmin(p["H"], fourpiM, D, p["d"]) + # surface (Damon-Eshbach, theta = pi/2) branch of film 1 at K_S + f_DE = float(film_dispersion_GHz(K_S, np.pi / 2, p["H"], fourpiM, D, p["d"])) + # second film (same material -> same 4piM, D) spectral minimum at thickness D2_FILM + fmin2, kmin2 = film_fmin(p["H"], fourpiM, D, D2_FILM) + return { + "fourpiM_G": fourpiM, + "D_Oecm2": D, + "f_min_GHz": fmin, + "k_min_percm": kmin, + "f_DE_GHz": f_DE, + "fmin2_GHz": fmin2, + "kmin2_percm": kmin2, + } + + +# ============================ PART B ============================ +# Config B1: Nature-06 YIG system + sub-critical pumping +NB = {"H": 700.0, "D": 2e-9} # Gauss, Oe cm^2 +T0 = 300.0 # K +NU_P = 4.0e9 # pumped magnon frequency (Hz) +DELTA_N = 5e18 # pumped magnon density (cm^-3) + +# Config B2: a second YIG system, supercritically pumped (condenses) +NB2 = {"H": 500.0, "D": 2e-9} +T0_2 = 300.0 +NU_P2 = 4.0e9 +DELTA_N2 = 1.5e20 # cm^-3 (chosen well above the B2 critical density) + +# Config B3: interlocked system that uses the exchange stiffness extracted in Part A +NB3 = {"H": 400.0} # D supplied from Part A at runtime +T0_3 = 300.0 +DELTA_N3 = 2e18 # cm^-3 + + +def _eps(k, H, D): + """3D bulk exchange magnon energy (J). k in cm^-1.""" + return H_PLANCK * GAMMA * (H + D * k**2) * 1e9 + + +def bulk_density(T, mu, H, D): + """3D bulk thermal magnon density (cm^-3): n = (1/2pi^2) int k^2 nBE dk.""" + val, _ = quad(lambda k: k**2 / np.expm1((_eps(k, H, D) - mu) / (KB * T)), 0, 3e8, limit=600) + return val / (2 * np.pi**2) + + +def bulk_energy(T, mu, H, D): + val, _ = quad(lambda k: k**2 * _eps(k, H, D) / np.expm1((_eps(k, H, D) - mu) / (KB * T)), 0, 3e8, limit=600) + return val / (2 * np.pi**2) + + +def _subcritical_solve(H, D, T0_cfg, nu_p, delta_n, n0, e0): + """Pumped sub-critical quasi-equilibrium: conserve number and energy -> (T, mu).""" + nT = n0 + delta_n + eT = e0 + H_PLANCK * nu_p * delta_n + eps_min = _eps(0.0, H, D) + sol = fsolve( + lambda v: [(bulk_density(v[0], v[1], H, D) - nT) / nT, (bulk_energy(v[0], v[1], H, D) - eT) / eT], + [T0_cfg, eps_min * 0.3], + full_output=True, + ) + (T_p, mu_p), ier = sol[0], sol[2] + return T_p, mu_p, bool(ier == 1) + + +def _condensed_solve(H, D, T0_cfg, nu_p, delta_n, n0, e0): + """Pumped supercritical branch: mu pinned at eps_min, conserve number+energy + for (T, condensate density n_c). Returns (T_p, condensate_fraction).""" + eps_min = _eps(0.0, H, D) + mu_pin = eps_min * (1 - 1e-12) + n_tot = n0 + delta_n + e_tot = e0 + H_PLANCK * nu_p * delta_n + + def f_T(T): + n_th = bulk_density(T, mu_pin, H, D) + return bulk_energy(T, mu_pin, H, D) + (n_tot - n_th) * eps_min - e_tot + + T_p = brentq(f_T, 20.0, 3.0 * T0_cfg) + n_c = n_tot - bulk_density(T_p, mu_pin, H, D) + return T_p, n_c / n_tot + + +def part_B(D_from_A=None): + # ---- Config B1 (sub-critical) ---- + H, D = NB["H"], NB["D"] + eps_min = _eps(0.0, H, D) # k=0 gap = h gamma H + n0 = bulk_density(T0, 0.0, H, D) + e0 = bulk_energy(T0, 0.0, H, D) + n_crit = bulk_density(T0, eps_min * (1 - 1e-9), H, D) # density at mu -> eps_min + dN_c = n_crit - n0 + T_p, mu_p, conv1 = _subcritical_solve(H, D, T0, NU_P, DELTA_N, n0, e0) + # Bloch exponent of the gapless 3D gas: n0(T) ~ T^p near T0 + p_bloch = float(np.log(bulk_density(2 * T0, 0.0, H, D) / n0) / np.log(2.0)) + + # ---- Config B2 (supercritical -> condensate) ---- + H2, D2 = NB2["H"], NB2["D"] + eps_min2 = _eps(0.0, H2, D2) + n0_2 = bulk_density(T0_2, 0.0, H2, D2) + e0_2 = bulk_energy(T0_2, 0.0, H2, D2) + dN_c2 = bulk_density(T0_2, eps_min2 * (1 - 1e-9), H2, D2) - n0_2 + bec2 = bool(DELTA_N2 > dN_c2) + T_p2, ncfrac2 = _condensed_solve(H2, D2, T0_2, NU_P2, DELTA_N2, n0_2, e0_2) + + # ---- Config B3 (A->B interlock: uses D extracted in Part A) ---- + D3 = D_from_A if D_from_A is not None else part_A()["D_Oecm2"] + H3 = NB3["H"] + eps_min3 = _eps(0.0, H3, D3) + n0_3 = bulk_density(T0_3, 0.0, H3, D3) + dN_c3 = bulk_density(T0_3, eps_min3 * (1 - 1e-9), H3, D3) - n0_3 + bec3 = bool(DELTA_N3 > dN_c3) + + return { + # config B1 + "eps_min_mK": eps_min / KB * 1e3, + "n0_percm3": n0, + "e0_Jpercm3": e0, + "p_bloch": p_bloch, + "dN_c_percm3": dN_c, + "mu_p_mK": mu_p / KB * 1e3, + "T_p_K": T_p, + "bec": bool(DELTA_N > dN_c), + "converged": conv1, + # config B2 (supercritical) + "eps_min2_mK": eps_min2 / KB * 1e3, + "dN_c2_percm3": dN_c2, + "bec2": bec2, + "ncfrac2": ncfrac2, + "T_p2_K": T_p2, + # config B3 (interlock) + "n0_3_percm3": n0_3, + "dN_c3_percm3": dN_c3, + "bec3": bec3, + } + + +if __name__ == "__main__": + import json + + A = part_A() + B = part_B(D_from_A=A["D_Oecm2"]) + print(json.dumps({"A": A, "B": B}, indent=2)) diff --git a/tasks/magnon-dispersion-bec/verifier/test.sh b/tasks/magnon-dispersion-bec/verifier/test.sh new file mode 100755 index 0000000..24c3d80 --- /dev/null +++ b/tasks/magnon-dispersion-bec/verifier/test.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -u +mkdir -p /logs/verifier +[ -f /root/result.md ] && cp /root/result.md /logs/verifier/result.md +[ -f /root/oracle_diagnostics.json ] && cp /root/oracle_diagnostics.json /logs/verifier/oracle_diagnostics.json +python3 -m pytest -p no:cacheprovider --ctrf /logs/verifier/ctrf.json /verifier/test_outputs.py -rA -v > /logs/verifier/output.txt 2>&1 +RC=$? +cat /logs/verifier/output.txt +if [ "$RC" -eq 0 ]; then echo 1 > /logs/verifier/reward.txt; else echo 0 > /logs/verifier/reward.txt; fi +exit 0 diff --git a/tasks/magnon-dispersion-bec/verifier/test_outputs.py b/tasks/magnon-dispersion-bec/verifier/test_outputs.py new file mode 100644 index 0000000..8d9edec --- /dev/null +++ b/tasks/magnon-dispersion-bec/verifier/test_outputs.py @@ -0,0 +1,150 @@ +"""Outcome checks for the YIG magnon dispersion + pumped-magnon-gas task. + +The verifier independently recomputes the reference physics (Rezende film +dispersion; 3D bulk magnon-gas thermodynamics, including the pumped sub-critical +and supercritical/condensed branches) and compares the agent's reported values +against it, with tolerances that accommodate reasonable numerical implementations +(quadrature, root finding). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import numpy as np +import pytest + +sys.path.insert(0, "/verifier") +import magnon_physics as mp + +ROOT = Path("/root") +RESULT_MD = ROOT / "result.md" +LOG = Path("/logs/verifier") + +_KEYS = ( + # Part A + "fourpiM_G", "D_Oecm2", "k_min_percm", "f_DE_GHz", "fmin2_GHz", "kmin2_percm", + # Part B config 1 (sub-critical) + "eps_min_mK", "n0_percm3", "e0_Jpercm3", "p_bloch", "dN_c_percm3", "mu_p_mK", "T_p_K", "bec", + # Part B config 2 (supercritical / condensed) + "eps_min2_mK", "dN_c2_percm3", "bec2", "ncfrac2", "T_p2_K", + # Part B config 3 (A->B interlock) + "n0_3_percm3", "dN_c3_percm3", "bec3", +) + + +def _parse(): + assert RESULT_MD.is_file(), f"missing deliverable: {RESULT_MD}" + m = re.search(r"RESULT\s*(.*?)\s*END", RESULT_MD.read_text(), re.DOTALL) + assert m, "result.md must contain a 'RESULT ... END' block" + body, vals = m.group(1), {} + for k in _KEYS: + km = re.search(rf"^\s*{re.escape(k)}\s*=\s*([-+0-9.eE]+)\s*$", body, re.MULTILINE) + assert km, f"RESULT block missing key: {k}" + vals[k] = float(km.group(1)) + return vals + + +@pytest.fixture(scope="session") +def sub(): + vals = _parse() + A = mp.part_A() + B = mp.part_B(D_from_A=A["D_Oecm2"]) + LOG.mkdir(parents=True, exist_ok=True) + (LOG / "reference.json").write_text(str({"A": A, "B": B})) + return {"vals": vals, "A": A, "B": B} + + +def test_partA_material_parameters(sub): + v, A = sub["vals"], sub["A"] + assert np.isclose(v["fourpiM_G"], A["fourpiM_G"], rtol=0.01), f"4piM {v['fourpiM_G']:.1f} != {A['fourpiM_G']:.1f} G (Kittel from f_FMR)" + assert np.isclose(v["D_Oecm2"], A["D_Oecm2"], rtol=0.04), ( + f"exchange stiffness D {v['D_Oecm2']:.3e} != {A['D_Oecm2']:.3e} (must reproduce f_min via the dipole-exchange film dispersion)" + ) + assert np.isclose(v["k_min_percm"], A["k_min_percm"], rtol=0.06), ( + f"spectral-minimum wavevector k_min {v['k_min_percm']:.3e} != {A['k_min_percm']:.3e}" + ) + + +def test_partA_surface_branch_and_second_film(sub): + v, A = sub["vals"], sub["A"] + assert np.isclose(v["f_DE_GHz"], A["f_DE_GHz"], rtol=0.02), ( + f"surface-branch frequency f_DE {v['f_DE_GHz']:.4f} != {A['f_DE_GHz']:.4f} GHz " + f"(theta=pi/2 branch requires the full dipolar form factor)" + ) + assert np.isclose(v["fmin2_GHz"], A["fmin2_GHz"], rtol=0.02), ( + f"second-film spectral minimum {v['fmin2_GHz']:.4f} != {A['fmin2_GHz']:.4f} GHz " + f"(same material, thickness 0.5 um)" + ) + assert np.isclose(v["kmin2_percm"], A["kmin2_percm"], rtol=0.06), ( + f"second-film minimum wavevector {v['kmin2_percm']:.3e} != {A['kmin2_percm']:.3e} cm^-1" + ) + + +def test_partB_thermal_gas(sub): + v, B = sub["vals"], sub["B"] + assert np.isclose(v["eps_min_mK"], B["eps_min_mK"], rtol=0.02), f"spectral gap eps_min/kB {v['eps_min_mK']:.1f} != {B['eps_min_mK']:.1f} mK" + assert np.isclose(v["n0_percm3"], B["n0_percm3"], rtol=0.05), ( + f"thermal magnon density n0 {v['n0_percm3']:.3e} != {B['n0_percm3']:.3e} cm^-3 (3D bulk exchange magnon gas, Bloch T^3/2)" + ) + assert np.isclose(v["e0_Jpercm3"], B["e0_Jpercm3"], rtol=0.05), ( + f"thermal energy density e0 {v['e0_Jpercm3']:.3e} != {B['e0_Jpercm3']:.3e} J/cm^3" + ) + assert np.isclose(v["p_bloch"], B["p_bloch"], atol=0.08), ( + f"Bloch exponent p {v['p_bloch']:.3f} != {B['p_bloch']:.3f} " + f"(gapless 3D exchange gas gives ~1.5; a 2D film would give ~1)" + ) + + +def test_partB_bec_threshold(sub): + v, B = sub["vals"], sub["B"] + assert int(v["bec"]) == int(B["bec"]), ( + f"BEC determination {int(v['bec'])} != {int(B['bec'])} (delta_N = 5e18 cm^-3 vs the critical pumped density)" + ) + assert np.isclose(v["dN_c_percm3"], B["dN_c_percm3"], rtol=0.30), ( + f"critical pumped density dN_c {v['dN_c_percm3']:.3e} != {B['dN_c_percm3']:.3e} cm^-3" + ) + + +def test_partB_pumped_state(sub): + v, B = sub["vals"], sub["B"] + assert np.isclose(v["mu_p_mK"], B["mu_p_mK"], rtol=0.30, atol=4.0), ( + f"pumped chemical potential mu/kB {v['mu_p_mK']:.2f} != {B['mu_p_mK']:.2f} mK" + ) + assert np.isclose(v["T_p_K"], B["T_p_K"], atol=3.0), f"pumped-gas temperature {v['T_p_K']:.1f} != {B['T_p_K']:.1f} K" + + +def test_partB_supercritical_condensate(sub): + v, B = sub["vals"], sub["B"] + assert np.isclose(v["eps_min2_mK"], B["eps_min2_mK"], rtol=0.02), ( + f"config-2 spectral gap {v['eps_min2_mK']:.1f} != {B['eps_min2_mK']:.1f} mK" + ) + assert np.isclose(v["dN_c2_percm3"], B["dN_c2_percm3"], rtol=0.30), ( + f"config-2 critical density dN_c2 {v['dN_c2_percm3']:.3e} != {B['dN_c2_percm3']:.3e} cm^-3" + ) + assert int(v["bec2"]) == int(B["bec2"]), ( + f"config-2 BEC determination {int(v['bec2'])} != {int(B['bec2'])} (delta_N2 = 1.5e20 cm^-3 is supercritical)" + ) + assert np.isclose(v["ncfrac2"], B["ncfrac2"], rtol=0.25, atol=0.005), ( + f"config-2 condensate fraction {v['ncfrac2']:.4f} != {B['ncfrac2']:.4f} " + f"(requires the condensed-branch solve with mu pinned at eps_min)" + ) + assert np.isclose(v["T_p2_K"], B["T_p2_K"], atol=5.0), ( + f"config-2 pumped-gas temperature {v['T_p2_K']:.1f} != {B['T_p2_K']:.1f} K" + ) + + +def test_partB_interlocked_config(sub): + v, B = sub["vals"], sub["B"] + assert np.isclose(v["n0_3_percm3"], B["n0_3_percm3"], rtol=0.06), ( + f"config-3 thermal density {v['n0_3_percm3']:.3e} != {B['n0_3_percm3']:.3e} cm^-3 " + f"(uses the exchange stiffness D extracted in Part A)" + ) + assert np.isclose(v["dN_c3_percm3"], B["dN_c3_percm3"], rtol=0.30), ( + f"config-3 critical density {v['dN_c3_percm3']:.3e} != {B['dN_c3_percm3']:.3e} cm^-3" + ) + assert int(v["bec3"]) == int(B["bec3"]), ( + f"config-3 BEC determination {int(v['bec3'])} != {int(B['bec3'])}" + )