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.
Files changed (233) hide show
  1. quantized/__init__.py +11 -0
  2. quantized/__main__.py +14 -0
  3. quantized/api.py +220 -0
  4. quantized/app.py +206 -0
  5. quantized/calc/__init__.py +8 -0
  6. quantized/calc/_clipfit.py +76 -0
  7. quantized/calc/_natural_neighbor.py +219 -0
  8. quantized/calc/aggregate.py +159 -0
  9. quantized/calc/backgrounds.py +353 -0
  10. quantized/calc/baseline.py +349 -0
  11. quantized/calc/batch_fit.py +148 -0
  12. quantized/calc/constants.py +27 -0
  13. quantized/calc/corrections.py +192 -0
  14. quantized/calc/crystallography.py +400 -0
  15. quantized/calc/diffusion.py +120 -0
  16. quantized/calc/electrical.py +248 -0
  17. quantized/calc/electrochemistry.py +176 -0
  18. quantized/calc/element_data.json +1 -0
  19. quantized/calc/element_data.py +59 -0
  20. quantized/calc/errors.py +246 -0
  21. quantized/calc/figure.py +417 -0
  22. quantized/calc/figure_break.py +152 -0
  23. quantized/calc/figure_categorical.py +156 -0
  24. quantized/calc/figure_corner.py +229 -0
  25. quantized/calc/figure_facets.py +127 -0
  26. quantized/calc/figure_field.py +137 -0
  27. quantized/calc/figure_hitmap.py +116 -0
  28. quantized/calc/figure_labels.py +62 -0
  29. quantized/calc/figure_map.py +287 -0
  30. quantized/calc/figure_overrides.py +159 -0
  31. quantized/calc/figure_page.py +266 -0
  32. quantized/calc/figure_scale.py +125 -0
  33. quantized/calc/figure_statplots.py +167 -0
  34. quantized/calc/figure_styles.py +131 -0
  35. quantized/calc/figure_ternary.py +239 -0
  36. quantized/calc/figure_ticks.py +217 -0
  37. quantized/calc/fit_autoguess.py +156 -0
  38. quantized/calc/fit_bootstrap.py +163 -0
  39. quantized/calc/fit_bumps.py +258 -0
  40. quantized/calc/fit_constraints.py +114 -0
  41. quantized/calc/fit_equation.py +264 -0
  42. quantized/calc/fit_findxy.py +80 -0
  43. quantized/calc/fit_models.py +195 -0
  44. quantized/calc/fit_models_special.py +189 -0
  45. quantized/calc/fit_odr.py +99 -0
  46. quantized/calc/fit_scan.py +243 -0
  47. quantized/calc/fit_stats.py +215 -0
  48. quantized/calc/fitting.py +199 -0
  49. quantized/calc/formula.py +90 -0
  50. quantized/calc/global_curve_fit.py +305 -0
  51. quantized/calc/global_fit.py +181 -0
  52. quantized/calc/interp2d.py +260 -0
  53. quantized/calc/linecut.py +269 -0
  54. quantized/calc/magnetic.py +414 -0
  55. quantized/calc/magnetometry.py +464 -0
  56. quantized/calc/map.py +228 -0
  57. quantized/calc/mcmc.py +177 -0
  58. quantized/calc/optics.py +228 -0
  59. quantized/calc/pawley.py +251 -0
  60. quantized/calc/peak_batch.py +128 -0
  61. quantized/calc/peak_fit.py +259 -0
  62. quantized/calc/peak_integrate.py +104 -0
  63. quantized/calc/peak_multifit.py +260 -0
  64. quantized/calc/peak_track.py +134 -0
  65. quantized/calc/peaks.py +298 -0
  66. quantized/calc/peakshapes.py +85 -0
  67. quantized/calc/plotting.py +147 -0
  68. quantized/calc/processing.py +232 -0
  69. quantized/calc/qspace.py +48 -0
  70. quantized/calc/reductions.py +155 -0
  71. quantized/calc/reductions_fft.py +383 -0
  72. quantized/calc/refl_sld_presets.json +1 -0
  73. quantized/calc/reflectivity.py +80 -0
  74. quantized/calc/registry.py +303 -0
  75. quantized/calc/relaxation.py +119 -0
  76. quantized/calc/report.py +253 -0
  77. quantized/calc/report_emit.py +227 -0
  78. quantized/calc/resample.py +142 -0
  79. quantized/calc/rsm.py +91 -0
  80. quantized/calc/rsm_analyze.py +245 -0
  81. quantized/calc/semiconductor.py +488 -0
  82. quantized/calc/sld.py +131 -0
  83. quantized/calc/sld_formula.py +138 -0
  84. quantized/calc/spectral.py +357 -0
  85. quantized/calc/statplots.py +214 -0
  86. quantized/calc/stats.py +399 -0
  87. quantized/calc/stats_anova2.py +202 -0
  88. quantized/calc/stats_anova_ext.py +338 -0
  89. quantized/calc/stats_dist.py +196 -0
  90. quantized/calc/stats_glm.py +245 -0
  91. quantized/calc/stats_multivar.py +289 -0
  92. quantized/calc/stats_roc.py +157 -0
  93. quantized/calc/stats_survival.py +261 -0
  94. quantized/calc/stats_tests.py +380 -0
  95. quantized/calc/substrates.py +181 -0
  96. quantized/calc/superconductor.py +359 -0
  97. quantized/calc/surface_fit.py +290 -0
  98. quantized/calc/surface_models.py +156 -0
  99. quantized/calc/thermal.py +119 -0
  100. quantized/calc/thin_film.py +425 -0
  101. quantized/calc/unit_convert.py +259 -0
  102. quantized/calc/units.py +80 -0
  103. quantized/calc/vacuum.py +290 -0
  104. quantized/calc/xray.py +169 -0
  105. quantized/cli.py +214 -0
  106. quantized/datastruct.py +153 -0
  107. quantized/io/__init__.py +11 -0
  108. quantized/io/_hdf5_layout.py +308 -0
  109. quantized/io/_jcamp_asdf.py +135 -0
  110. quantized/io/_xrdml_scan.py +291 -0
  111. quantized/io/base.py +82 -0
  112. quantized/io/bruker_brml.py +177 -0
  113. quantized/io/bruker_raw.py +158 -0
  114. quantized/io/cif.py +266 -0
  115. quantized/io/consolidated.py +122 -0
  116. quantized/io/delimited.py +222 -0
  117. quantized/io/excel.py +135 -0
  118. quantized/io/hdf5.py +192 -0
  119. quantized/io/import_filters.py +178 -0
  120. quantized/io/import_preview.py +262 -0
  121. quantized/io/jcamp.py +179 -0
  122. quantized/io/lakeshore.py +163 -0
  123. quantized/io/ncnr.py +278 -0
  124. quantized/io/netcdf.py +195 -0
  125. quantized/io/opus.py +231 -0
  126. quantized/io/origin.py +346 -0
  127. quantized/io/origin_com.py +194 -0
  128. quantized/io/origin_project/__init__.py +221 -0
  129. quantized/io/origin_project/annotation_marks.py +288 -0
  130. quantized/io/origin_project/container.py +262 -0
  131. quantized/io/origin_project/curve_style_color.py +359 -0
  132. quantized/io/origin_project/figure_geometry.py +108 -0
  133. quantized/io/origin_project/figure_layers.py +333 -0
  134. quantized/io/origin_project/figure_text.py +258 -0
  135. quantized/io/origin_project/figures.py +210 -0
  136. quantized/io/origin_project/figures_opju.py +440 -0
  137. quantized/io/origin_project/notes.py +302 -0
  138. quantized/io/origin_project/opj.py +459 -0
  139. quantized/io/origin_project/opj_curves.py +297 -0
  140. quantized/io/origin_project/opj_shapes.py +148 -0
  141. quantized/io/origin_project/opju.py +146 -0
  142. quantized/io/origin_project/opju_axis_real_form.py +418 -0
  143. quantized/io/origin_project/opju_axis_specimen_form.py +167 -0
  144. quantized/io/origin_project/opju_codec.py +370 -0
  145. quantized/io/origin_project/opju_curves.py +497 -0
  146. quantized/io/origin_project/opju_curves_allcols.py +258 -0
  147. quantized/io/origin_project/opju_figure_curves.py +302 -0
  148. quantized/io/origin_project/opju_figure_text.py +245 -0
  149. quantized/io/origin_project/opju_reports.py +129 -0
  150. quantized/io/origin_project/origin_richtext.py +145 -0
  151. quantized/io/origin_project/preview.py +132 -0
  152. quantized/io/origin_project/templates.py +314 -0
  153. quantized/io/origin_project/tree.py +379 -0
  154. quantized/io/origin_project/tree_opju.py +228 -0
  155. quantized/io/origin_project/windows.py +238 -0
  156. quantized/io/origin_project/windows_opju.py +393 -0
  157. quantized/io/origin_project/writer.py +156 -0
  158. quantized/io/origin_project/writer_blocks.py +282 -0
  159. quantized/io/qd.py +380 -0
  160. quantized/io/refl1d.py +132 -0
  161. quantized/io/registry.py +210 -0
  162. quantized/io/report_export.py +347 -0
  163. quantized/io/rigaku.py +100 -0
  164. quantized/io/sims.py +398 -0
  165. quantized/io/spc.py +311 -0
  166. quantized/io/xrd_csv.py +308 -0
  167. quantized/io/xrdml.py +394 -0
  168. quantized/jobs.py +173 -0
  169. quantized/plugins/__init__.py +50 -0
  170. quantized/plugins/contract.py +111 -0
  171. quantized/plugins/loader.py +394 -0
  172. quantized/plugins/steps.py +90 -0
  173. quantized/routes/__init__.py +7 -0
  174. quantized/routes/_bookcache.py +62 -0
  175. quantized/routes/_export_common.py +27 -0
  176. quantized/routes/_payload.py +58 -0
  177. quantized/routes/_uploadcache.py +59 -0
  178. quantized/routes/aggregate.py +46 -0
  179. quantized/routes/baseline.py +210 -0
  180. quantized/routes/books.py +117 -0
  181. quantized/routes/calc.py +56 -0
  182. quantized/routes/corrections.py +78 -0
  183. quantized/routes/crystallography.py +80 -0
  184. quantized/routes/diffusion.py +58 -0
  185. quantized/routes/electrical.py +101 -0
  186. quantized/routes/electrochemistry.py +83 -0
  187. quantized/routes/export.py +280 -0
  188. quantized/routes/export_facets.py +83 -0
  189. quantized/routes/export_figures.py +471 -0
  190. quantized/routes/export_page.py +125 -0
  191. quantized/routes/fitting.py +379 -0
  192. quantized/routes/fitting_bumps.py +97 -0
  193. quantized/routes/import_template.py +97 -0
  194. quantized/routes/import_wizard.py +150 -0
  195. quantized/routes/jobs_api.py +59 -0
  196. quantized/routes/magnetic.py +135 -0
  197. quantized/routes/magnetometry.py +133 -0
  198. quantized/routes/optics.py +98 -0
  199. quantized/routes/parsers.py +281 -0
  200. quantized/routes/peaks.py +184 -0
  201. quantized/routes/plot.py +103 -0
  202. quantized/routes/reductions.py +121 -0
  203. quantized/routes/reference.py +58 -0
  204. quantized/routes/reflectivity.py +91 -0
  205. quantized/routes/report_export.py +119 -0
  206. quantized/routes/rsm.py +136 -0
  207. quantized/routes/samples.py +32 -0
  208. quantized/routes/semiconductor.py +207 -0
  209. quantized/routes/sld.py +42 -0
  210. quantized/routes/spectral.py +54 -0
  211. quantized/routes/statplots.py +99 -0
  212. quantized/routes/stats.py +418 -0
  213. quantized/routes/stats_design.py +321 -0
  214. quantized/routes/substrates.py +46 -0
  215. quantized/routes/superconductor.py +139 -0
  216. quantized/routes/thermal.py +57 -0
  217. quantized/routes/thin_film.py +153 -0
  218. quantized/routes/vacuum.py +113 -0
  219. quantized/routes/xray.py +32 -0
  220. quantized/samples/demo_vsm.csv +42 -0
  221. quantized/server_launch.py +251 -0
  222. quantized/web/assets/JetBrainsMono-Bold-CUogYd9I.woff2 +0 -0
  223. quantized/web/assets/JetBrainsMono-Regular-CA-Os4ii.woff2 +0 -0
  224. quantized/web/assets/index-BHmmCL-x.js +27 -0
  225. quantized/web/assets/index-BiZzN7J6.css +1 -0
  226. quantized/web/index.html +13 -0
  227. quantized/web/loading.html +69 -0
  228. quantized_lab-0.8.0.dist-info/METADATA +122 -0
  229. quantized_lab-0.8.0.dist-info/RECORD +233 -0
  230. quantized_lab-0.8.0.dist-info/WHEEL +4 -0
  231. quantized_lab-0.8.0.dist-info/entry_points.txt +4 -0
  232. quantized_lab-0.8.0.dist-info/licenses/LICENSE +201 -0
  233. 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