quantized-lab 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantized/__init__.py +11 -0
- quantized/__main__.py +14 -0
- quantized/api.py +220 -0
- quantized/app.py +206 -0
- quantized/calc/__init__.py +8 -0
- quantized/calc/_clipfit.py +76 -0
- quantized/calc/_natural_neighbor.py +219 -0
- quantized/calc/aggregate.py +159 -0
- quantized/calc/backgrounds.py +353 -0
- quantized/calc/baseline.py +349 -0
- quantized/calc/batch_fit.py +148 -0
- quantized/calc/constants.py +27 -0
- quantized/calc/corrections.py +192 -0
- quantized/calc/crystallography.py +400 -0
- quantized/calc/diffusion.py +120 -0
- quantized/calc/electrical.py +248 -0
- quantized/calc/electrochemistry.py +176 -0
- quantized/calc/element_data.json +1 -0
- quantized/calc/element_data.py +59 -0
- quantized/calc/errors.py +246 -0
- quantized/calc/figure.py +417 -0
- quantized/calc/figure_break.py +152 -0
- quantized/calc/figure_categorical.py +156 -0
- quantized/calc/figure_corner.py +229 -0
- quantized/calc/figure_facets.py +127 -0
- quantized/calc/figure_field.py +137 -0
- quantized/calc/figure_hitmap.py +116 -0
- quantized/calc/figure_labels.py +62 -0
- quantized/calc/figure_map.py +287 -0
- quantized/calc/figure_overrides.py +159 -0
- quantized/calc/figure_page.py +266 -0
- quantized/calc/figure_scale.py +125 -0
- quantized/calc/figure_statplots.py +167 -0
- quantized/calc/figure_styles.py +131 -0
- quantized/calc/figure_ternary.py +239 -0
- quantized/calc/figure_ticks.py +217 -0
- quantized/calc/fit_autoguess.py +156 -0
- quantized/calc/fit_bootstrap.py +163 -0
- quantized/calc/fit_bumps.py +258 -0
- quantized/calc/fit_constraints.py +114 -0
- quantized/calc/fit_equation.py +264 -0
- quantized/calc/fit_findxy.py +80 -0
- quantized/calc/fit_models.py +195 -0
- quantized/calc/fit_models_special.py +189 -0
- quantized/calc/fit_odr.py +99 -0
- quantized/calc/fit_scan.py +243 -0
- quantized/calc/fit_stats.py +215 -0
- quantized/calc/fitting.py +199 -0
- quantized/calc/formula.py +90 -0
- quantized/calc/global_curve_fit.py +305 -0
- quantized/calc/global_fit.py +181 -0
- quantized/calc/interp2d.py +260 -0
- quantized/calc/linecut.py +269 -0
- quantized/calc/magnetic.py +414 -0
- quantized/calc/magnetometry.py +464 -0
- quantized/calc/map.py +228 -0
- quantized/calc/mcmc.py +177 -0
- quantized/calc/optics.py +228 -0
- quantized/calc/pawley.py +251 -0
- quantized/calc/peak_batch.py +128 -0
- quantized/calc/peak_fit.py +259 -0
- quantized/calc/peak_integrate.py +104 -0
- quantized/calc/peak_multifit.py +260 -0
- quantized/calc/peak_track.py +134 -0
- quantized/calc/peaks.py +298 -0
- quantized/calc/peakshapes.py +85 -0
- quantized/calc/plotting.py +147 -0
- quantized/calc/processing.py +232 -0
- quantized/calc/qspace.py +48 -0
- quantized/calc/reductions.py +155 -0
- quantized/calc/reductions_fft.py +383 -0
- quantized/calc/refl_sld_presets.json +1 -0
- quantized/calc/reflectivity.py +80 -0
- quantized/calc/registry.py +303 -0
- quantized/calc/relaxation.py +119 -0
- quantized/calc/report.py +253 -0
- quantized/calc/report_emit.py +227 -0
- quantized/calc/resample.py +142 -0
- quantized/calc/rsm.py +91 -0
- quantized/calc/rsm_analyze.py +245 -0
- quantized/calc/semiconductor.py +488 -0
- quantized/calc/sld.py +131 -0
- quantized/calc/sld_formula.py +138 -0
- quantized/calc/spectral.py +357 -0
- quantized/calc/statplots.py +214 -0
- quantized/calc/stats.py +399 -0
- quantized/calc/stats_anova2.py +202 -0
- quantized/calc/stats_anova_ext.py +338 -0
- quantized/calc/stats_dist.py +196 -0
- quantized/calc/stats_glm.py +245 -0
- quantized/calc/stats_multivar.py +289 -0
- quantized/calc/stats_roc.py +157 -0
- quantized/calc/stats_survival.py +261 -0
- quantized/calc/stats_tests.py +380 -0
- quantized/calc/substrates.py +181 -0
- quantized/calc/superconductor.py +359 -0
- quantized/calc/surface_fit.py +290 -0
- quantized/calc/surface_models.py +156 -0
- quantized/calc/thermal.py +119 -0
- quantized/calc/thin_film.py +425 -0
- quantized/calc/unit_convert.py +259 -0
- quantized/calc/units.py +80 -0
- quantized/calc/vacuum.py +290 -0
- quantized/calc/xray.py +169 -0
- quantized/cli.py +214 -0
- quantized/datastruct.py +153 -0
- quantized/io/__init__.py +11 -0
- quantized/io/_hdf5_layout.py +308 -0
- quantized/io/_jcamp_asdf.py +135 -0
- quantized/io/_xrdml_scan.py +291 -0
- quantized/io/base.py +82 -0
- quantized/io/bruker_brml.py +177 -0
- quantized/io/bruker_raw.py +158 -0
- quantized/io/cif.py +266 -0
- quantized/io/consolidated.py +122 -0
- quantized/io/delimited.py +222 -0
- quantized/io/excel.py +135 -0
- quantized/io/hdf5.py +192 -0
- quantized/io/import_filters.py +178 -0
- quantized/io/import_preview.py +262 -0
- quantized/io/jcamp.py +179 -0
- quantized/io/lakeshore.py +163 -0
- quantized/io/ncnr.py +278 -0
- quantized/io/netcdf.py +195 -0
- quantized/io/opus.py +231 -0
- quantized/io/origin.py +346 -0
- quantized/io/origin_com.py +194 -0
- quantized/io/origin_project/__init__.py +221 -0
- quantized/io/origin_project/annotation_marks.py +288 -0
- quantized/io/origin_project/container.py +262 -0
- quantized/io/origin_project/curve_style_color.py +359 -0
- quantized/io/origin_project/figure_geometry.py +108 -0
- quantized/io/origin_project/figure_layers.py +333 -0
- quantized/io/origin_project/figure_text.py +258 -0
- quantized/io/origin_project/figures.py +210 -0
- quantized/io/origin_project/figures_opju.py +440 -0
- quantized/io/origin_project/notes.py +302 -0
- quantized/io/origin_project/opj.py +459 -0
- quantized/io/origin_project/opj_curves.py +297 -0
- quantized/io/origin_project/opj_shapes.py +148 -0
- quantized/io/origin_project/opju.py +146 -0
- quantized/io/origin_project/opju_axis_real_form.py +418 -0
- quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
- quantized/io/origin_project/opju_codec.py +370 -0
- quantized/io/origin_project/opju_curves.py +497 -0
- quantized/io/origin_project/opju_curves_allcols.py +258 -0
- quantized/io/origin_project/opju_figure_curves.py +302 -0
- quantized/io/origin_project/opju_figure_text.py +245 -0
- quantized/io/origin_project/opju_reports.py +129 -0
- quantized/io/origin_project/origin_richtext.py +145 -0
- quantized/io/origin_project/preview.py +132 -0
- quantized/io/origin_project/templates.py +314 -0
- quantized/io/origin_project/tree.py +379 -0
- quantized/io/origin_project/tree_opju.py +228 -0
- quantized/io/origin_project/windows.py +238 -0
- quantized/io/origin_project/windows_opju.py +393 -0
- quantized/io/origin_project/writer.py +156 -0
- quantized/io/origin_project/writer_blocks.py +282 -0
- quantized/io/qd.py +380 -0
- quantized/io/refl1d.py +132 -0
- quantized/io/registry.py +210 -0
- quantized/io/report_export.py +347 -0
- quantized/io/rigaku.py +100 -0
- quantized/io/sims.py +398 -0
- quantized/io/spc.py +311 -0
- quantized/io/xrd_csv.py +308 -0
- quantized/io/xrdml.py +394 -0
- quantized/jobs.py +173 -0
- quantized/plugins/__init__.py +50 -0
- quantized/plugins/contract.py +111 -0
- quantized/plugins/loader.py +394 -0
- quantized/plugins/steps.py +90 -0
- quantized/routes/__init__.py +7 -0
- quantized/routes/_bookcache.py +62 -0
- quantized/routes/_export_common.py +27 -0
- quantized/routes/_payload.py +58 -0
- quantized/routes/_uploadcache.py +59 -0
- quantized/routes/aggregate.py +46 -0
- quantized/routes/baseline.py +210 -0
- quantized/routes/books.py +117 -0
- quantized/routes/calc.py +56 -0
- quantized/routes/corrections.py +78 -0
- quantized/routes/crystallography.py +80 -0
- quantized/routes/diffusion.py +58 -0
- quantized/routes/electrical.py +101 -0
- quantized/routes/electrochemistry.py +83 -0
- quantized/routes/export.py +280 -0
- quantized/routes/export_facets.py +83 -0
- quantized/routes/export_figures.py +471 -0
- quantized/routes/export_page.py +125 -0
- quantized/routes/fitting.py +379 -0
- quantized/routes/fitting_bumps.py +97 -0
- quantized/routes/import_template.py +97 -0
- quantized/routes/import_wizard.py +150 -0
- quantized/routes/jobs_api.py +59 -0
- quantized/routes/magnetic.py +135 -0
- quantized/routes/magnetometry.py +133 -0
- quantized/routes/optics.py +98 -0
- quantized/routes/parsers.py +281 -0
- quantized/routes/peaks.py +184 -0
- quantized/routes/plot.py +103 -0
- quantized/routes/reductions.py +121 -0
- quantized/routes/reference.py +58 -0
- quantized/routes/reflectivity.py +91 -0
- quantized/routes/report_export.py +119 -0
- quantized/routes/rsm.py +136 -0
- quantized/routes/samples.py +32 -0
- quantized/routes/semiconductor.py +207 -0
- quantized/routes/sld.py +42 -0
- quantized/routes/spectral.py +54 -0
- quantized/routes/statplots.py +99 -0
- quantized/routes/stats.py +418 -0
- quantized/routes/stats_design.py +321 -0
- quantized/routes/substrates.py +46 -0
- quantized/routes/superconductor.py +139 -0
- quantized/routes/thermal.py +57 -0
- quantized/routes/thin_film.py +153 -0
- quantized/routes/vacuum.py +113 -0
- quantized/routes/xray.py +32 -0
- quantized/samples/demo_vsm.csv +42 -0
- quantized/server_launch.py +251 -0
- quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
- quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
- quantized/web/assets/index-BHmmCL-x.js +27 -0
- quantized/web/assets/index-BiZzN7J6.css +1 -0
- quantized/web/index.html +13 -0
- quantized/web/loading.html +69 -0
- quantized_lab-0.8.0.dist-info/METADATA +122 -0
- quantized_lab-0.8.0.dist-info/RECORD +233 -0
- quantized_lab-0.8.0.dist-info/WHEEL +4 -0
- quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
- quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
- quantized_lab-0.8.0.dist-info/licenses/NOTICE +11 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
r"""Semiconductor device-physics calculators (DiraCulator ``buildSemiconductorTab``
|
|
2
|
+
+ ``+calc/+semiconductor``).
|
|
3
|
+
|
|
4
|
+
Pure calc layer — closed-form scalars in, result dicts out. No fastapi /
|
|
5
|
+
pydantic imports. Ports the MATLAB ``calc.semiconductor`` functions verbatim
|
|
6
|
+
(the MATLAB ``latex`` field is intentionally omitted):
|
|
7
|
+
|
|
8
|
+
.. math::
|
|
9
|
+
|
|
10
|
+
n_i = \sqrt{N_c N_v}\,e^{-E_g/2k_BT} \qquad
|
|
11
|
+
N_{c,v} = 2\left(\tfrac{2\pi m^* k_B T}{h^2}\right)^{3/2} \\
|
|
12
|
+
n = \tfrac12\!\left(\Delta + \sqrt{\Delta^2 + 4 n_i^2}\right),\;
|
|
13
|
+
p = n_i^2/n,\; \Delta = N_d - N_a \\
|
|
14
|
+
E_F - E_i = k_BT\,\operatorname{asinh}\!\big(\Delta/2n_i\big) \qquad
|
|
15
|
+
V_{bi} = \tfrac{k_BT}{q}\ln\!\big(N_a N_d/n_i^2\big) \\
|
|
16
|
+
W = \sqrt{\tfrac{2\varepsilon_0\varepsilon_r}{q}
|
|
17
|
+
\big(\tfrac1{N_a}+\tfrac1{N_d}\big)V_{bi}'} \qquad
|
|
18
|
+
L_D = \sqrt{\tfrac{\varepsilon_0\varepsilon_r k_BT}{q^2 n}} \\
|
|
19
|
+
D = \mu k_BT/q \qquad L = \sqrt{D\tau} \qquad
|
|
20
|
+
v_{th} = \sqrt{3 k_BT/m^*} \qquad
|
|
21
|
+
R_H = \tfrac1q\tfrac{p\mu_h^2 - n\mu_e^2}{(p\mu_h + n\mu_e)^2}
|
|
22
|
+
|
|
23
|
+
Units follow the MATLAB toolbox (the behavioural reference): concentrations in
|
|
24
|
+
cm⁻³, lengths reported in both cm and nm, temperatures in K (never °C), masses
|
|
25
|
+
as ``m*/m₀`` (dimensionless), mobilities in cm²/(V·s), velocities in cm/s.
|
|
26
|
+
|
|
27
|
+
Material presets (300 K) — Eg [eV], εᵣ, mₑ*, m_h* (in m₀):
|
|
28
|
+
Si, Ge, GaAs, InP, GaN, SiC, SiO₂, Al₂O₃.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import math
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from quantized.calc.constants import constants
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"MATERIALS",
|
|
40
|
+
"built_in_potential",
|
|
41
|
+
"carrier_concentration",
|
|
42
|
+
"debye_length",
|
|
43
|
+
"depletion_width",
|
|
44
|
+
"diffusion_coeff",
|
|
45
|
+
"diffusion_length",
|
|
46
|
+
"dos_effective_mass",
|
|
47
|
+
"fermi_level",
|
|
48
|
+
"hall_coefficient",
|
|
49
|
+
"intrinsic_carrier_conc",
|
|
50
|
+
"material_presets",
|
|
51
|
+
"mobility_model",
|
|
52
|
+
"sheet_carrier_density",
|
|
53
|
+
"thermal_velocity",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Common semiconductor material parameters (calc.semiconductor.materialPresets).
|
|
57
|
+
# Eg [eV] @ 300 K, eps_r, me*/m0, mh*/m0 (NaN where unavailable). Do-not-"fix":
|
|
58
|
+
# these are calibrated literature values from the MATLAB reference.
|
|
59
|
+
MATERIALS: dict[str, dict[str, float | str]] = {
|
|
60
|
+
"Si": {"Eg": 1.12, "eps_r": 11.7, "me": 1.08, "mh": 0.81, "name": "Silicon"},
|
|
61
|
+
"Ge": {"Eg": 0.66, "eps_r": 16.0, "me": 0.55, "mh": 0.37, "name": "Germanium"},
|
|
62
|
+
"GaAs": {"Eg": 1.42, "eps_r": 12.9, "me": 0.067, "mh": 0.45, "name": "Gallium Arsenide"},
|
|
63
|
+
"InP": {"Eg": 1.35, "eps_r": 12.5, "me": 0.08, "mh": 0.6, "name": "Indium Phosphide"},
|
|
64
|
+
"GaN": {"Eg": 3.4, "eps_r": 8.9, "me": 0.2, "mh": 1.4, "name": "Gallium Nitride"},
|
|
65
|
+
"SiC": {"Eg": 3.26, "eps_r": 9.7, "me": 0.37, "mh": 1.0, "name": "4H-SiC"},
|
|
66
|
+
"SiO2": {"Eg": 9.0, "eps_r": 3.9, "me": 0.5, "mh": float("nan"), "name": "Silicon Dioxide"},
|
|
67
|
+
"Al2O3": {"Eg": 8.8, "eps_r": 9.0, "me": 0.4, "mh": float("nan"), "name": "Sapphire"},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def material_presets() -> dict[str, dict[str, float | str]]:
|
|
72
|
+
"""Return the semiconductor material-parameter table (``materialPresets.m``).
|
|
73
|
+
|
|
74
|
+
Each entry has ``Eg`` (eV), ``eps_r``, ``me`` and ``mh`` (in m₀), ``name``.
|
|
75
|
+
|
|
76
|
+
>>> material_presets()["GaAs"]["Eg"]
|
|
77
|
+
1.42
|
|
78
|
+
"""
|
|
79
|
+
return {k: dict(v) for k, v in MATERIALS.items()}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _preset(material: str) -> dict[str, float | str]:
|
|
83
|
+
"""Look up a material preset, raising ValueError for unknown names."""
|
|
84
|
+
try:
|
|
85
|
+
return MATERIALS[material]
|
|
86
|
+
except KeyError as exc:
|
|
87
|
+
raise ValueError(f"unknown material '{material}'") from exc
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _doping_type(net: float, ni: float) -> str:
|
|
91
|
+
"""Doping type from the net donor density (intrinsic within 0.1·n_i)."""
|
|
92
|
+
if abs(net) < 0.1 * ni:
|
|
93
|
+
return "intrinsic"
|
|
94
|
+
return "n" if net > 0 else "p"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def intrinsic_carrier_conc(
|
|
98
|
+
eg: float | None = None,
|
|
99
|
+
me_star: float | None = None,
|
|
100
|
+
mh_star: float | None = None,
|
|
101
|
+
t: float = 300.0,
|
|
102
|
+
material: str | None = None,
|
|
103
|
+
) -> dict[str, float]:
|
|
104
|
+
"""Intrinsic carrier concentration n_i (``intrinsicCarrierConc.m``).
|
|
105
|
+
|
|
106
|
+
``N_{c,v} = 2(2π m* k_B T / h²)^{3/2}`` (effective DOS, m⁻³ → cm⁻³) and
|
|
107
|
+
``n_i = √(N_c N_v)·exp(−E_g q / 2k_B T)``. A ``material`` name fills any of
|
|
108
|
+
``eg``/``me_star``/``mh_star`` left as ``None`` from the preset table.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
eg: band gap (eV); from ``material`` if omitted.
|
|
112
|
+
me_star: electron DOS effective mass (m₀); from ``material`` if omitted.
|
|
113
|
+
mh_star: hole DOS effective mass (m₀); from ``material`` if omitted.
|
|
114
|
+
t: temperature (K), > 0.
|
|
115
|
+
material: preset name (e.g. ``'Si'``) to auto-fill the above.
|
|
116
|
+
|
|
117
|
+
Returns ``ni``, ``Nc``, ``Nv`` (cm⁻³), ``Eg`` (eV), ``T`` (K).
|
|
118
|
+
|
|
119
|
+
>>> r = intrinsic_carrier_conc(material="Si")
|
|
120
|
+
>>> 5e9 < r["ni"] < 2e10
|
|
121
|
+
True
|
|
122
|
+
"""
|
|
123
|
+
if material is not None:
|
|
124
|
+
mat = _preset(material)
|
|
125
|
+
if eg is None:
|
|
126
|
+
eg = float(mat["Eg"])
|
|
127
|
+
if me_star is None:
|
|
128
|
+
me_star = float(mat["me"])
|
|
129
|
+
if mh_star is None:
|
|
130
|
+
mh_star = float(mat["mh"])
|
|
131
|
+
if eg is None or me_star is None or mh_star is None:
|
|
132
|
+
raise ValueError("provide eg, me_star, mh_star or a valid material name")
|
|
133
|
+
if eg < 0:
|
|
134
|
+
# A negative band gap is unphysical and makes exp(-eg·…) overflow below.
|
|
135
|
+
raise ValueError("band gap Eg must be non-negative")
|
|
136
|
+
if t <= 0:
|
|
137
|
+
raise ValueError("T must be positive")
|
|
138
|
+
if me_star <= 0 or mh_star <= 0 or math.isnan(me_star) or math.isnan(mh_star):
|
|
139
|
+
raise ValueError("effective masses must be positive")
|
|
140
|
+
|
|
141
|
+
c = constants()
|
|
142
|
+
me_kg = me_star * c["m_e"]
|
|
143
|
+
mh_kg = mh_star * c["m_e"]
|
|
144
|
+
nc_m3 = 2.0 * (2.0 * math.pi * me_kg * c["kB"] * t / c["h"] ** 2) ** 1.5
|
|
145
|
+
nv_m3 = 2.0 * (2.0 * math.pi * mh_kg * c["kB"] * t / c["h"] ** 2) ** 1.5
|
|
146
|
+
nc = nc_m3 * 1e-6
|
|
147
|
+
nv = nv_m3 * 1e-6
|
|
148
|
+
ni = math.sqrt(nc * nv) * math.exp(-eg * c["e"] / (2.0 * c["kB"] * t))
|
|
149
|
+
return {"ni": ni, "Nc": nc, "Nv": nv, "Eg": eg, "T": t}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def carrier_concentration(nd: float, na: float, ni: float) -> dict[str, Any]:
|
|
153
|
+
"""Majority/minority carrier concentrations (``carrierConcentration.m``).
|
|
154
|
+
|
|
155
|
+
Exact charge-neutrality + mass-action solution (Sze §1.5):
|
|
156
|
+
``n = ½(Δ + √(Δ² + 4n_i²))``, ``p = n_i²/n`` with ``Δ = N_d − N_a``. This
|
|
157
|
+
interpolates smoothly between the intrinsic and extrinsic regimes.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
nd: donor concentration (cm⁻³), >= 0.
|
|
161
|
+
na: acceptor concentration (cm⁻³), >= 0.
|
|
162
|
+
ni: intrinsic carrier concentration (cm⁻³), > 0.
|
|
163
|
+
|
|
164
|
+
Returns ``n``, ``p`` (cm⁻³) and ``type`` (``'n'``/``'p'``/``'intrinsic'``).
|
|
165
|
+
|
|
166
|
+
>>> r = carrier_concentration(1e16, 0.0, 1.5e10)
|
|
167
|
+
>>> round(r["n"] / 1e16, 4), r["type"]
|
|
168
|
+
(1.0, 'n')
|
|
169
|
+
"""
|
|
170
|
+
if nd < 0 or na < 0:
|
|
171
|
+
raise ValueError("Nd and Na must be non-negative")
|
|
172
|
+
if ni <= 0:
|
|
173
|
+
raise ValueError("ni must be positive")
|
|
174
|
+
net = nd - na
|
|
175
|
+
n = 0.5 * (net + math.sqrt(net**2 + 4.0 * ni**2))
|
|
176
|
+
p = ni**2 / n
|
|
177
|
+
return {"n": n, "p": p, "type": _doping_type(net, ni)}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def built_in_potential(na: float, nd: float, ni: float, t: float = 300.0) -> dict[str, float]:
|
|
181
|
+
"""Built-in potential of a p-n junction (``builtInPotential.m``).
|
|
182
|
+
|
|
183
|
+
``V_bi = (k_B T / q)·ln(N_a N_d / n_i²)`` (V).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
na: acceptor concentration (cm⁻³), > 0.
|
|
187
|
+
nd: donor concentration (cm⁻³), > 0.
|
|
188
|
+
ni: intrinsic carrier concentration (cm⁻³), > 0.
|
|
189
|
+
t: temperature (K), > 0.
|
|
190
|
+
|
|
191
|
+
>>> round(built_in_potential(1e17, 1e17, 9.65e9, 300.0)["Vbi"], 3)
|
|
192
|
+
0.835
|
|
193
|
+
"""
|
|
194
|
+
if na <= 0 or nd <= 0 or ni <= 0:
|
|
195
|
+
raise ValueError("Na, Nd and ni must be positive")
|
|
196
|
+
if t <= 0:
|
|
197
|
+
raise ValueError("T must be positive")
|
|
198
|
+
c = constants()
|
|
199
|
+
kt = c["kB"] * t / c["e"]
|
|
200
|
+
return {"Vbi": kt * math.log(na * nd / ni**2)}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def depletion_width(
|
|
204
|
+
vbi: float,
|
|
205
|
+
na: float,
|
|
206
|
+
nd: float,
|
|
207
|
+
epsilon_r: float | None = None,
|
|
208
|
+
material: str | None = None,
|
|
209
|
+
t: float = 300.0,
|
|
210
|
+
) -> dict[str, float]:
|
|
211
|
+
"""Depletion width of a p-n junction (``depletionWidth.m``).
|
|
212
|
+
|
|
213
|
+
Sze Ch. 2.2 with the ``−2k_BT/q`` correction to V_bi (the majority-carrier
|
|
214
|
+
distribution tails): ``W = √(2ε₀εᵣ(1/N_a + 1/N_d)·V_bi'/q)`` with
|
|
215
|
+
``V_bi' = max(V_bi − 2k_BT/q, 0)``. ``x_n``/``x_p`` are partitioned by charge
|
|
216
|
+
neutrality. A ``material`` name fills ``epsilon_r`` if omitted.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
vbi: built-in potential (V), > 0.
|
|
220
|
+
na: acceptor doping on the p-side (cm⁻³), > 0.
|
|
221
|
+
nd: donor doping on the n-side (cm⁻³), > 0.
|
|
222
|
+
epsilon_r: relative permittivity; from ``material`` if omitted.
|
|
223
|
+
material: preset name (e.g. ``'Si'``) to auto-fill ``epsilon_r``.
|
|
224
|
+
t: temperature (K) for the kT/q correction, > 0.
|
|
225
|
+
|
|
226
|
+
Returns ``W``, ``xn``, ``xp`` (nm) and ``Wcm`` (cm).
|
|
227
|
+
"""
|
|
228
|
+
if material is not None and epsilon_r is None:
|
|
229
|
+
epsilon_r = float(_preset(material)["eps_r"])
|
|
230
|
+
if epsilon_r is None:
|
|
231
|
+
raise ValueError("provide epsilon_r or a valid material name")
|
|
232
|
+
if vbi <= 0 or na <= 0 or nd <= 0:
|
|
233
|
+
raise ValueError("Vbi, Na and Nd must be positive")
|
|
234
|
+
if t <= 0:
|
|
235
|
+
raise ValueError("T must be positive")
|
|
236
|
+
c = constants()
|
|
237
|
+
na_m3 = na * 1e6
|
|
238
|
+
nd_m3 = nd * 1e6
|
|
239
|
+
kt_over_q = c["kB"] * t / c["e"]
|
|
240
|
+
vbi_eff = max(vbi - 2.0 * kt_over_q, 0.0)
|
|
241
|
+
w_m = math.sqrt(2.0 * c["eps0"] * epsilon_r * (1.0 / na_m3 + 1.0 / nd_m3) * vbi_eff / c["e"])
|
|
242
|
+
w_nm = w_m * 1e9
|
|
243
|
+
xn = w_nm * na / (na + nd)
|
|
244
|
+
xp = w_nm * nd / (na + nd)
|
|
245
|
+
return {"W": w_nm, "Wcm": w_m * 100.0, "xn": xn, "xp": xp}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def diffusion_coeff(mu: float, t: float = 300.0) -> dict[str, float]:
|
|
249
|
+
"""Diffusion coefficient via the Einstein relation (``diffusionCoeff.m``).
|
|
250
|
+
|
|
251
|
+
``D = μ·k_B T / q`` (cm²/s).
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
mu: carrier mobility (cm²/V·s), > 0.
|
|
255
|
+
t: temperature (K), > 0.
|
|
256
|
+
|
|
257
|
+
>>> round(diffusion_coeff(1400.0, 300.0)["D"], 3)
|
|
258
|
+
36.193
|
|
259
|
+
"""
|
|
260
|
+
if mu <= 0:
|
|
261
|
+
raise ValueError("mu must be positive")
|
|
262
|
+
if t <= 0:
|
|
263
|
+
raise ValueError("T must be positive")
|
|
264
|
+
c = constants()
|
|
265
|
+
return {"D": mu * c["kB"] * t / c["e"], "mu": mu, "T": t}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def diffusion_length(d: float, tau: float) -> dict[str, float]:
|
|
269
|
+
"""Minority-carrier diffusion length (``diffusionLength.m``).
|
|
270
|
+
|
|
271
|
+
``L = √(D·τ)`` (cm); ``Lum = L·10⁴`` (µm).
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
d: diffusion coefficient (cm²/s), > 0.
|
|
275
|
+
tau: minority-carrier lifetime (s), > 0.
|
|
276
|
+
|
|
277
|
+
>>> round(diffusion_length(25.0, 1e-6)["Lum"], 3)
|
|
278
|
+
50.0
|
|
279
|
+
"""
|
|
280
|
+
if d <= 0:
|
|
281
|
+
raise ValueError("D must be positive")
|
|
282
|
+
if tau <= 0:
|
|
283
|
+
raise ValueError("tau must be positive")
|
|
284
|
+
length = math.sqrt(d * tau)
|
|
285
|
+
return {"L": length, "Lum": length * 1e4, "D": d, "tau": tau}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def dos_effective_mass(material: str, carrier: str = "e") -> dict[str, Any]:
|
|
289
|
+
"""DOS effective mass for a material + carrier type (``dosEffectiveMass.m``).
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
material: preset name (e.g. ``'GaAs'``).
|
|
293
|
+
carrier: ``'e'`` (electrons) or ``'h'`` (holes).
|
|
294
|
+
|
|
295
|
+
Returns ``mStar`` (m₀), ``material``, ``carrier``.
|
|
296
|
+
|
|
297
|
+
>>> dos_effective_mass("GaAs", "e")["mStar"]
|
|
298
|
+
0.067
|
|
299
|
+
"""
|
|
300
|
+
if carrier not in ("e", "h"):
|
|
301
|
+
raise ValueError("carrier must be 'e' or 'h'")
|
|
302
|
+
mat = _preset(material)
|
|
303
|
+
m_star = float(mat["me"] if carrier == "e" else mat["mh"])
|
|
304
|
+
if math.isnan(m_star):
|
|
305
|
+
raise ValueError(f"hole effective mass not available for {material}")
|
|
306
|
+
return {"mStar": m_star, "material": material, "carrier": carrier}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def fermi_level(
|
|
310
|
+
eg: float | None = None,
|
|
311
|
+
me_star: float | None = None,
|
|
312
|
+
mh_star: float | None = None,
|
|
313
|
+
nd: float = 0.0,
|
|
314
|
+
na: float = 0.0,
|
|
315
|
+
t: float = 300.0,
|
|
316
|
+
material: str | None = None,
|
|
317
|
+
) -> dict[str, Any]:
|
|
318
|
+
"""Fermi level relative to the intrinsic level E_i (``fermiLevel.m``).
|
|
319
|
+
|
|
320
|
+
From charge-neutrality + mass-action: ``E_F − E_i = k_BT·asinh(Δ/2n_i)``
|
|
321
|
+
with ``Δ = N_d − N_a`` (Sze §1.5). Positive = above E_i (n-type). The
|
|
322
|
+
Boltzmann (non-degenerate) approximation breaks down within ~3kT of a band
|
|
323
|
+
edge — use a Fermi-Dirac solver for degenerate doping.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
eg: band gap (eV); from ``material`` if omitted.
|
|
327
|
+
me_star, mh_star: DOS effective masses (m₀); from ``material`` if omitted.
|
|
328
|
+
nd: donor concentration (cm⁻³), >= 0.
|
|
329
|
+
na: acceptor concentration (cm⁻³), >= 0.
|
|
330
|
+
t: temperature (K), > 0.
|
|
331
|
+
material: preset name to auto-fill ``eg``/``me_star``/``mh_star``.
|
|
332
|
+
|
|
333
|
+
Returns ``EF`` (eV, relative to E_i) and ``type``.
|
|
334
|
+
"""
|
|
335
|
+
if nd < 0 or na < 0:
|
|
336
|
+
raise ValueError("Nd and Na must be non-negative")
|
|
337
|
+
ni = intrinsic_carrier_conc(eg, me_star, mh_star, t, material)["ni"]
|
|
338
|
+
c = constants()
|
|
339
|
+
kt = c["kB"] * t / c["e"]
|
|
340
|
+
net = nd - na
|
|
341
|
+
ef = kt * math.asinh(net / (2.0 * ni))
|
|
342
|
+
return {"EF": ef, "type": _doping_type(net, ni)}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def hall_coefficient(n: float, p: float, mu_e: float, mu_h: float) -> dict[str, Any]:
|
|
346
|
+
"""Hall coefficient for mixed conduction (``hallCoefficient.m``).
|
|
347
|
+
|
|
348
|
+
``R_H = (1/q)·(p μ_h² − n μ_e²) / (p μ_h + n μ_e)²`` (cm³/C). Sign gives the
|
|
349
|
+
apparent carrier type: ``R_H < 0`` → ``'n'``, else ``'p'``.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
n: electron concentration (cm⁻³), >= 0.
|
|
353
|
+
p: hole concentration (cm⁻³), >= 0.
|
|
354
|
+
mu_e: electron mobility (cm²/V·s), > 0.
|
|
355
|
+
mu_h: hole mobility (cm²/V·s), > 0.
|
|
356
|
+
|
|
357
|
+
>>> hall_coefficient(1e16, 1e4, 1400.0, 450.0)["apparent_type"]
|
|
358
|
+
'n'
|
|
359
|
+
"""
|
|
360
|
+
if n < 0 or p < 0:
|
|
361
|
+
raise ValueError("n and p must be non-negative")
|
|
362
|
+
if mu_e <= 0 or mu_h <= 0:
|
|
363
|
+
raise ValueError("mobilities must be positive")
|
|
364
|
+
denom = p * mu_h + n * mu_e
|
|
365
|
+
if denom == 0:
|
|
366
|
+
raise ValueError("p·μ_h + n·μ_e must be non-zero")
|
|
367
|
+
q = constants()["e"]
|
|
368
|
+
r_h = (1.0 / q) * (p * mu_h**2 - n * mu_e**2) / denom**2
|
|
369
|
+
return {"RH": r_h, "apparent_type": "n" if r_h < 0 else "p"}
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def debye_length(
|
|
373
|
+
n: float,
|
|
374
|
+
epsilon_r: float | None = None,
|
|
375
|
+
t: float = 300.0,
|
|
376
|
+
material: str | None = None,
|
|
377
|
+
) -> dict[str, float]:
|
|
378
|
+
"""Debye screening length in a semiconductor (``debyeLength.m``).
|
|
379
|
+
|
|
380
|
+
``L_D = √(ε₀ εᵣ k_B T / (q² n))``. A ``material`` name fills ``epsilon_r``.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
n: carrier concentration (cm⁻³), > 0.
|
|
384
|
+
epsilon_r: relative permittivity; from ``material`` if omitted.
|
|
385
|
+
t: temperature (K), > 0.
|
|
386
|
+
material: preset name to auto-fill ``epsilon_r``.
|
|
387
|
+
|
|
388
|
+
Returns ``LD`` (nm) and ``LDcm`` (cm).
|
|
389
|
+
"""
|
|
390
|
+
if material is not None and epsilon_r is None:
|
|
391
|
+
epsilon_r = float(_preset(material)["eps_r"])
|
|
392
|
+
if epsilon_r is None:
|
|
393
|
+
raise ValueError("provide epsilon_r or a valid material name")
|
|
394
|
+
if n <= 0:
|
|
395
|
+
raise ValueError("n must be positive")
|
|
396
|
+
if t <= 0:
|
|
397
|
+
raise ValueError("T must be positive")
|
|
398
|
+
c = constants()
|
|
399
|
+
n_m3 = n * 1e6
|
|
400
|
+
ld_m = math.sqrt(c["eps0"] * epsilon_r * c["kB"] * t / (c["e"] ** 2 * n_m3))
|
|
401
|
+
return {"LD": ld_m * 1e9, "LDcm": ld_m * 100.0}
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Si Caughey-Thomas coefficients (Sze empirical). Do-not-"fix": calibrated.
|
|
405
|
+
_CT_SI = {
|
|
406
|
+
"muMin_e": 88.0,
|
|
407
|
+
"muMax_e": 1252.0,
|
|
408
|
+
"Nref_e": 1.26e17,
|
|
409
|
+
"alpha_e": 0.88,
|
|
410
|
+
"beta_e": -2.4,
|
|
411
|
+
"muMin_h": 54.0,
|
|
412
|
+
"muMax_h": 407.0,
|
|
413
|
+
"Nref_h": 2.35e17,
|
|
414
|
+
"alpha_h": 0.88,
|
|
415
|
+
"beta_h": -2.2,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def mobility_model(material: str = "Si", t: float = 300.0, n: float = 0.0) -> dict[str, Any]:
|
|
420
|
+
"""Caughey-Thomas doping/temperature-dependent mobility (``mobilityModel.m``).
|
|
421
|
+
|
|
422
|
+
``μ = μ_min + (μ_max − μ_min)/(1 + (N/N_ref)^α)`` scaled by ``(T/300)^β``.
|
|
423
|
+
Only Si is parameterised; other materials fall back to the Si coefficients.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
material: material name (Si coefficients used as a fallback otherwise).
|
|
427
|
+
t: temperature (K), > 0.
|
|
428
|
+
n: total impurity concentration N_d + N_a (cm⁻³), >= 0.
|
|
429
|
+
|
|
430
|
+
Returns ``muE``, ``muH`` (cm²/V·s) and ``material``.
|
|
431
|
+
|
|
432
|
+
>>> round(mobility_model("Si", 300.0, 1e16)["muE"], 1)
|
|
433
|
+
1139.0
|
|
434
|
+
"""
|
|
435
|
+
if t <= 0:
|
|
436
|
+
raise ValueError("T must be positive")
|
|
437
|
+
if n < 0:
|
|
438
|
+
raise ValueError("N must be non-negative")
|
|
439
|
+
k = _CT_SI
|
|
440
|
+
n_eff = max(n, 1.0) # avoid divide-by-zero; N→0 gives μ_max
|
|
441
|
+
|
|
442
|
+
def _ct(mn: float, mx: float, nref: float, alpha: float, beta: float) -> float:
|
|
443
|
+
mu_lattice = mn + (mx - mn) / (1.0 + (n_eff / nref) ** alpha)
|
|
444
|
+
return float(mu_lattice * (t / 300.0) ** beta)
|
|
445
|
+
|
|
446
|
+
mu_e = _ct(k["muMin_e"], k["muMax_e"], k["Nref_e"], k["alpha_e"], k["beta_e"])
|
|
447
|
+
mu_h = _ct(k["muMin_h"], k["muMax_h"], k["Nref_h"], k["alpha_h"], k["beta_h"])
|
|
448
|
+
return {"muE": mu_e, "muH": mu_h, "material": material}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def sheet_carrier_density(n: float, t: float) -> dict[str, float]:
|
|
452
|
+
"""Sheet carrier density from bulk concentration and thickness (``sheetCarrierDensity.m``).
|
|
453
|
+
|
|
454
|
+
``n_s = n·t`` (cm⁻²).
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
n: bulk carrier concentration (cm⁻³), > 0.
|
|
458
|
+
t: layer thickness (cm), > 0.
|
|
459
|
+
|
|
460
|
+
>>> sheet_carrier_density(1e17, 1e-6)["ns"]
|
|
461
|
+
100000000000.0
|
|
462
|
+
"""
|
|
463
|
+
if n <= 0:
|
|
464
|
+
raise ValueError("n must be positive")
|
|
465
|
+
if t <= 0:
|
|
466
|
+
raise ValueError("t must be positive")
|
|
467
|
+
return {"ns": n * t, "n": n, "t": t}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def thermal_velocity(m_star: float, t: float = 300.0) -> dict[str, float]:
|
|
471
|
+
"""Thermal velocity of carriers (``thermalVelocity.m``).
|
|
472
|
+
|
|
473
|
+
``v_th = √(3 k_B T / (m* m₀))`` (cm/s).
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
m_star: effective mass in units of m₀, > 0.
|
|
477
|
+
t: temperature (K), > 0.
|
|
478
|
+
|
|
479
|
+
>>> 1e7 < thermal_velocity(0.26, 300.0)["vth"] < 5e7
|
|
480
|
+
True
|
|
481
|
+
"""
|
|
482
|
+
if m_star <= 0:
|
|
483
|
+
raise ValueError("m_star must be positive")
|
|
484
|
+
if t <= 0:
|
|
485
|
+
raise ValueError("T must be positive")
|
|
486
|
+
c = constants()
|
|
487
|
+
vth_m = math.sqrt(3.0 * c["kB"] * t / (m_star * c["m_e"]))
|
|
488
|
+
return {"vth": vth_m * 100.0, "mStar": m_star, "T": t}
|
quantized/calc/sld.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""SLD-depth-profile helpers for reflectivity. Ports of +fitting SLD functions.
|
|
2
|
+
|
|
3
|
+
Pure calc layer: sld_profile (layer stack -> error-function SLD(z)), spline_sld
|
|
4
|
+
(knots -> interpolated SLD(z)), profile_to_layers (SLD(z) -> discrete layers), and
|
|
5
|
+
refl_sld_presets (material SLD table, loaded from JSON for exact data parity).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
from numpy.typing import ArrayLike, NDArray
|
|
17
|
+
from scipy.special import erf
|
|
18
|
+
|
|
19
|
+
from .resample import _interp_column
|
|
20
|
+
|
|
21
|
+
__all__ = ["profile_to_layers", "refl_sld_presets", "sld_profile", "spline_sld"]
|
|
22
|
+
|
|
23
|
+
_PRESETS_PATH = Path(__file__).parent / "refl_sld_presets.json"
|
|
24
|
+
_PRESETS: list[dict[str, Any]] | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def sld_profile(
|
|
28
|
+
layers: ArrayLike, *, n_points: int = 500, padding: float = 50.0
|
|
29
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
30
|
+
"""Layer stack -> (z, SLD) with error-function interfaces. Port of sldProfile."""
|
|
31
|
+
lay = np.asarray(layers, dtype=float)
|
|
32
|
+
if lay.ndim != 2 or lay.shape[0] < 1 or lay.shape[1] < 4:
|
|
33
|
+
raise ValueError("layers must be a (N, 4) array [thickness, sld, _, roughness], N >= 1")
|
|
34
|
+
n_layers = lay.shape[0]
|
|
35
|
+
d, sld_r, sigma = lay[:, 0], lay[:, 1], lay[:, 3]
|
|
36
|
+
total_thick = float(np.sum(d[1:-1]))
|
|
37
|
+
z = np.linspace(-padding, total_thick + padding, n_points)
|
|
38
|
+
|
|
39
|
+
interface_z = np.zeros(n_layers)
|
|
40
|
+
for j in range(1, n_layers - 1):
|
|
41
|
+
interface_z[j] = interface_z[j - 1] + d[j]
|
|
42
|
+
interface_z[n_layers - 1] = total_thick
|
|
43
|
+
|
|
44
|
+
sld = np.full(z.shape, sld_r[0])
|
|
45
|
+
for j in range(1, n_layers):
|
|
46
|
+
sig = max(sigma[j], 0.5)
|
|
47
|
+
d_sld = sld_r[j] - sld_r[j - 1]
|
|
48
|
+
sld = sld + d_sld * 0.5 * (1 + erf((z - interface_z[j - 1]) / (sig * math.sqrt(2))))
|
|
49
|
+
return np.asarray(z, dtype=float), np.asarray(sld, dtype=float)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def spline_sld(
|
|
53
|
+
z_knots: ArrayLike,
|
|
54
|
+
sld_knots: ArrayLike,
|
|
55
|
+
*,
|
|
56
|
+
sld_ambient: float = float("nan"),
|
|
57
|
+
sld_substrate: float = float("nan"),
|
|
58
|
+
z_range: tuple[float, float] | None = None,
|
|
59
|
+
n_points: int = 500,
|
|
60
|
+
method: str = "pchip",
|
|
61
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
62
|
+
"""Knot interpolation -> (z, SLD), flat ambient/substrate outside. Port of splineSLD."""
|
|
63
|
+
zk = np.asarray(z_knots, dtype=float).ravel()
|
|
64
|
+
sk = np.asarray(sld_knots, dtype=float).ravel()
|
|
65
|
+
if zk.size < 2:
|
|
66
|
+
raise ValueError("need at least 2 knots")
|
|
67
|
+
if sk.size != zk.size:
|
|
68
|
+
raise ValueError("zKnots and sldKnots must be the same length")
|
|
69
|
+
if np.any(np.diff(zk) <= 0):
|
|
70
|
+
raise ValueError("zKnots must be strictly increasing")
|
|
71
|
+
method = method.lower()
|
|
72
|
+
if method not in ("pchip", "spline", "makima", "linear"):
|
|
73
|
+
raise ValueError("method must be pchip/spline/makima/linear")
|
|
74
|
+
|
|
75
|
+
sa = sk[0] if math.isnan(sld_ambient) else sld_ambient
|
|
76
|
+
ss = sk[-1] if math.isnan(sld_substrate) else sld_substrate
|
|
77
|
+
lo, hi = (zk[0] - 50, zk[-1] + 50) if z_range is None else (z_range[0], z_range[1])
|
|
78
|
+
if hi <= lo:
|
|
79
|
+
raise ValueError("z_range[1] must exceed z_range[0]")
|
|
80
|
+
|
|
81
|
+
z = np.linspace(lo, hi, n_points)
|
|
82
|
+
sld = np.zeros(z.shape)
|
|
83
|
+
inside = (z >= zk[0]) & (z <= zk[-1])
|
|
84
|
+
if np.any(inside):
|
|
85
|
+
sld[inside] = _interp_column(zk, sk, z[inside], method, False)
|
|
86
|
+
sld[z < zk[0]] = sa
|
|
87
|
+
sld[z > zk[-1]] = ss
|
|
88
|
+
return np.asarray(z, dtype=float), np.asarray(sld, dtype=float)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def profile_to_layers(
|
|
92
|
+
z: ArrayLike,
|
|
93
|
+
sld: ArrayLike,
|
|
94
|
+
*,
|
|
95
|
+
imag_sld: ArrayLike | None = None,
|
|
96
|
+
sld_ambient: float = float("nan"),
|
|
97
|
+
sld_substrate: float = float("nan"),
|
|
98
|
+
) -> NDArray[np.float64]:
|
|
99
|
+
"""SLD(z) profile -> discrete (M,4) layer stack (midpoint slabs). Port of profileToLayers."""
|
|
100
|
+
zv = np.asarray(z, dtype=float).ravel()
|
|
101
|
+
sldv = np.asarray(sld, dtype=float).ravel()
|
|
102
|
+
n = zv.size
|
|
103
|
+
if n < 2:
|
|
104
|
+
raise ValueError("need at least 2 profile points")
|
|
105
|
+
if sldv.size != n:
|
|
106
|
+
raise ValueError("sld must match z length")
|
|
107
|
+
if np.any(np.diff(zv) <= 0):
|
|
108
|
+
raise ValueError("z must be strictly increasing")
|
|
109
|
+
imag = np.zeros(n) if imag_sld is None else np.asarray(imag_sld, dtype=float).ravel()
|
|
110
|
+
if imag.size != n:
|
|
111
|
+
raise ValueError("imag_sld must match z length")
|
|
112
|
+
|
|
113
|
+
sa = sldv[0] if math.isnan(sld_ambient) else sld_ambient
|
|
114
|
+
ss = sldv[-1] if math.isnan(sld_substrate) else sld_substrate
|
|
115
|
+
interior = np.column_stack([
|
|
116
|
+
np.diff(zv),
|
|
117
|
+
0.5 * (sldv[:-1] + sldv[1:]),
|
|
118
|
+
0.5 * (imag[:-1] + imag[1:]),
|
|
119
|
+
np.zeros(n - 1),
|
|
120
|
+
])
|
|
121
|
+
top = np.array([[0.0, sa, 0.0, 0.0]])
|
|
122
|
+
bot = np.array([[0.0, ss, 0.0, 0.0]])
|
|
123
|
+
return np.asarray(np.vstack([top, interior, bot]), dtype=float)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def refl_sld_presets() -> list[dict[str, Any]]:
|
|
127
|
+
"""Material SLD presets (name/formula/sldX/sldN/sldImag/density). Port of reflSLDPresets."""
|
|
128
|
+
global _PRESETS
|
|
129
|
+
if _PRESETS is None:
|
|
130
|
+
_PRESETS = json.loads(_PRESETS_PATH.read_text(encoding="utf-8"))
|
|
131
|
+
return _PRESETS
|