chemistrykit 0.1.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 (294) hide show
  1. chemistrykit/__init__.py +69 -0
  2. chemistrykit/analytical/__init__.py +61 -0
  3. chemistrykit/analytical/core/__init__.py +0 -0
  4. chemistrykit/analytical/core/base_system.py +118 -0
  5. chemistrykit/analytical/systems/__init__.py +0 -0
  6. chemistrykit/analytical/systems/calibration.py +131 -0
  7. chemistrykit/analytical/systems/chromatography.py +317 -0
  8. chemistrykit/analytical/systems/qtest.py +128 -0
  9. chemistrykit/analytical/systems/titration.py +240 -0
  10. chemistrykit/analytical/systems/uncertainty.py +189 -0
  11. chemistrykit/analytical/tests/__init__.py +0 -0
  12. chemistrykit/analytical/tests/test_calibration.py +49 -0
  13. chemistrykit/analytical/tests/test_chromatography.py +77 -0
  14. chemistrykit/analytical/tests/test_qtest.py +66 -0
  15. chemistrykit/analytical/tests/test_titration.py +83 -0
  16. chemistrykit/analytical/tests/test_uncertainty.py +54 -0
  17. chemistrykit/analytical/tests/test_visualizers.py +56 -0
  18. chemistrykit/analytical/utils/__init__.py +0 -0
  19. chemistrykit/analytical/utils/regression.py +67 -0
  20. chemistrykit/analytical/visualizers/__init__.py +0 -0
  21. chemistrykit/analytical/visualizers/analytical_plots.py +129 -0
  22. chemistrykit/constants.py +208 -0
  23. chemistrykit/crystal/__init__.py +50 -0
  24. chemistrykit/crystal/core/__init__.py +0 -0
  25. chemistrykit/crystal/core/base_system.py +165 -0
  26. chemistrykit/crystal/systems/__init__.py +0 -0
  27. chemistrykit/crystal/systems/crystal_systems.py +158 -0
  28. chemistrykit/crystal/systems/defects.py +125 -0
  29. chemistrykit/crystal/systems/lattice_energy.py +150 -0
  30. chemistrykit/crystal/systems/madelung.py +96 -0
  31. chemistrykit/crystal/systems/packing.py +166 -0
  32. chemistrykit/crystal/systems/xrd.py +222 -0
  33. chemistrykit/crystal/tests/__init__.py +0 -0
  34. chemistrykit/crystal/tests/test_crystal_systems.py +39 -0
  35. chemistrykit/crystal/tests/test_defects.py +54 -0
  36. chemistrykit/crystal/tests/test_lattice_energy.py +54 -0
  37. chemistrykit/crystal/tests/test_madelung.py +41 -0
  38. chemistrykit/crystal/tests/test_packing.py +73 -0
  39. chemistrykit/crystal/tests/test_visualizers.py +43 -0
  40. chemistrykit/crystal/tests/test_xrd.py +90 -0
  41. chemistrykit/crystal/utils/__init__.py +0 -0
  42. chemistrykit/crystal/utils/lattice_sums.py +89 -0
  43. chemistrykit/crystal/utils/reference_data.py +84 -0
  44. chemistrykit/crystal/visualizers/__init__.py +0 -0
  45. chemistrykit/crystal/visualizers/crystal_plots.py +118 -0
  46. chemistrykit/electrochem/__init__.py +76 -0
  47. chemistrykit/electrochem/core/__init__.py +0 -0
  48. chemistrykit/electrochem/core/base_system.py +117 -0
  49. chemistrykit/electrochem/systems/__init__.py +0 -0
  50. chemistrykit/electrochem/systems/battery.py +183 -0
  51. chemistrykit/electrochem/systems/butler_volmer.py +306 -0
  52. chemistrykit/electrochem/systems/electrolysis.py +214 -0
  53. chemistrykit/electrochem/systems/nernst.py +268 -0
  54. chemistrykit/electrochem/systems/standard_potentials.py +221 -0
  55. chemistrykit/electrochem/tests/__init__.py +0 -0
  56. chemistrykit/electrochem/tests/test_battery.py +68 -0
  57. chemistrykit/electrochem/tests/test_butler_volmer.py +84 -0
  58. chemistrykit/electrochem/tests/test_electrolysis.py +61 -0
  59. chemistrykit/electrochem/tests/test_nernst.py +83 -0
  60. chemistrykit/electrochem/tests/test_standard_potentials.py +58 -0
  61. chemistrykit/electrochem/tests/test_visualizers.py +42 -0
  62. chemistrykit/electrochem/utils/__init__.py +0 -0
  63. chemistrykit/electrochem/utils/regression.py +57 -0
  64. chemistrykit/electrochem/visualizers/__init__.py +0 -0
  65. chemistrykit/electrochem/visualizers/electrochem_plots.py +121 -0
  66. chemistrykit/integrators/__init__.py +55 -0
  67. chemistrykit/integrators/adaptive.py +201 -0
  68. chemistrykit/integrators/fixed_step.py +352 -0
  69. chemistrykit/integrators/tests/__init__.py +0 -0
  70. chemistrykit/integrators/tests/test_integrators.py +114 -0
  71. chemistrykit/kinetics/__init__.py +55 -0
  72. chemistrykit/kinetics/core/__init__.py +0 -0
  73. chemistrykit/kinetics/core/base_system.py +247 -0
  74. chemistrykit/kinetics/core/integrators.py +28 -0
  75. chemistrykit/kinetics/systems/__init__.py +0 -0
  76. chemistrykit/kinetics/systems/arrhenius.py +120 -0
  77. chemistrykit/kinetics/systems/enzyme.py +269 -0
  78. chemistrykit/kinetics/systems/networks.py +362 -0
  79. chemistrykit/kinetics/systems/oscillators.py +131 -0
  80. chemistrykit/kinetics/systems/rate_laws.py +192 -0
  81. chemistrykit/kinetics/tests/__init__.py +0 -0
  82. chemistrykit/kinetics/tests/test_arrhenius.py +48 -0
  83. chemistrykit/kinetics/tests/test_enzyme.py +89 -0
  84. chemistrykit/kinetics/tests/test_networks.py +102 -0
  85. chemistrykit/kinetics/tests/test_oscillators.py +48 -0
  86. chemistrykit/kinetics/tests/test_rate_laws.py +69 -0
  87. chemistrykit/kinetics/tests/test_visualizers.py +57 -0
  88. chemistrykit/kinetics/utils/__init__.py +0 -0
  89. chemistrykit/kinetics/utils/linear_regression.py +56 -0
  90. chemistrykit/kinetics/visualizers/__init__.py +0 -0
  91. chemistrykit/kinetics/visualizers/kinetics_plots.py +147 -0
  92. chemistrykit/md/__init__.py +57 -0
  93. chemistrykit/md/core/__init__.py +1 -0
  94. chemistrykit/md/core/base_system.py +363 -0
  95. chemistrykit/md/systems/__init__.py +2 -0
  96. chemistrykit/md/systems/lj_fluid.py +466 -0
  97. chemistrykit/md/systems/pair_potentials.py +550 -0
  98. chemistrykit/md/systems/thermostats.py +172 -0
  99. chemistrykit/md/tests/__init__.py +0 -0
  100. chemistrykit/md/tests/test_lj_fluid.py +108 -0
  101. chemistrykit/md/tests/test_pair_potentials.py +198 -0
  102. chemistrykit/md/tests/test_pbc.py +77 -0
  103. chemistrykit/md/tests/test_thermostats.py +55 -0
  104. chemistrykit/md/tests/test_visualizers.py +47 -0
  105. chemistrykit/md/utils/__init__.py +1 -0
  106. chemistrykit/md/utils/neighbor_list.py +123 -0
  107. chemistrykit/md/utils/pbc.py +88 -0
  108. chemistrykit/md/visualizers/__init__.py +2 -0
  109. chemistrykit/md/visualizers/md_plots.py +146 -0
  110. chemistrykit/periodic_table.py +341 -0
  111. chemistrykit/photochem/__init__.py +58 -0
  112. chemistrykit/photochem/core/__init__.py +0 -0
  113. chemistrykit/photochem/core/base_system.py +54 -0
  114. chemistrykit/photochem/systems/__init__.py +0 -0
  115. chemistrykit/photochem/systems/jablonski.py +157 -0
  116. chemistrykit/photochem/systems/photostationary_state.py +186 -0
  117. chemistrykit/photochem/systems/quantum_yield.py +226 -0
  118. chemistrykit/photochem/systems/stern_volmer.py +224 -0
  119. chemistrykit/photochem/tests/__init__.py +0 -0
  120. chemistrykit/photochem/tests/test_jablonski.py +56 -0
  121. chemistrykit/photochem/tests/test_photostationary_state.py +75 -0
  122. chemistrykit/photochem/tests/test_quantum_yield.py +69 -0
  123. chemistrykit/photochem/tests/test_stern_volmer.py +71 -0
  124. chemistrykit/photochem/tests/test_visualizers.py +41 -0
  125. chemistrykit/photochem/utils/__init__.py +0 -0
  126. chemistrykit/photochem/utils/regression.py +56 -0
  127. chemistrykit/photochem/visualizers/__init__.py +0 -0
  128. chemistrykit/photochem/visualizers/photochem_plots.py +109 -0
  129. chemistrykit/polymer/__init__.py +70 -0
  130. chemistrykit/polymer/core/__init__.py +0 -0
  131. chemistrykit/polymer/core/base_system.py +106 -0
  132. chemistrykit/polymer/systems/__init__.py +0 -0
  133. chemistrykit/polymer/systems/chain_growth.py +253 -0
  134. chemistrykit/polymer/systems/chain_statistics.py +222 -0
  135. chemistrykit/polymer/systems/molecular_weight_distribution.py +293 -0
  136. chemistrykit/polymer/systems/step_growth.py +145 -0
  137. chemistrykit/polymer/tests/__init__.py +0 -0
  138. chemistrykit/polymer/tests/test_chain_growth.py +90 -0
  139. chemistrykit/polymer/tests/test_chain_statistics.py +97 -0
  140. chemistrykit/polymer/tests/test_molecular_weight_distribution.py +94 -0
  141. chemistrykit/polymer/tests/test_step_growth.py +36 -0
  142. chemistrykit/polymer/tests/test_visualizers.py +47 -0
  143. chemistrykit/polymer/utils/__init__.py +0 -0
  144. chemistrykit/polymer/utils/moments.py +84 -0
  145. chemistrykit/polymer/visualizers/__init__.py +0 -0
  146. chemistrykit/polymer/visualizers/polymer_plots.py +134 -0
  147. chemistrykit/quantum/__init__.py +72 -0
  148. chemistrykit/quantum/core/__init__.py +0 -0
  149. chemistrykit/quantum/core/base_system.py +194 -0
  150. chemistrykit/quantum/systems/__init__.py +0 -0
  151. chemistrykit/quantum/systems/harmonic_oscillator.py +281 -0
  152. chemistrykit/quantum/systems/hartree_fock.py +242 -0
  153. chemistrykit/quantum/systems/huckel.py +342 -0
  154. chemistrykit/quantum/systems/hydrogenlike.py +260 -0
  155. chemistrykit/quantum/systems/particle_in_box.py +287 -0
  156. chemistrykit/quantum/systems/perturbation.py +258 -0
  157. chemistrykit/quantum/systems/rigid_rotor.py +146 -0
  158. chemistrykit/quantum/tests/__init__.py +0 -0
  159. chemistrykit/quantum/tests/test_harmonic_oscillator.py +99 -0
  160. chemistrykit/quantum/tests/test_hartree_fock.py +78 -0
  161. chemistrykit/quantum/tests/test_huckel.py +112 -0
  162. chemistrykit/quantum/tests/test_hydrogenlike.py +98 -0
  163. chemistrykit/quantum/tests/test_particle_in_box.py +109 -0
  164. chemistrykit/quantum/tests/test_perturbation.py +105 -0
  165. chemistrykit/quantum/tests/test_rigid_rotor.py +53 -0
  166. chemistrykit/quantum/tests/test_secular_equation.py +63 -0
  167. chemistrykit/quantum/tests/test_visualizers.py +56 -0
  168. chemistrykit/quantum/utils/__init__.py +0 -0
  169. chemistrykit/quantum/utils/basis_sets.py +223 -0
  170. chemistrykit/quantum/utils/secular_equation.py +116 -0
  171. chemistrykit/quantum/visualizers/__init__.py +0 -0
  172. chemistrykit/quantum/visualizers/quantum_plots.py +188 -0
  173. chemistrykit/solutions/__init__.py +65 -0
  174. chemistrykit/solutions/core/__init__.py +0 -0
  175. chemistrykit/solutions/core/base_system.py +154 -0
  176. chemistrykit/solutions/systems/__init__.py +0 -0
  177. chemistrykit/solutions/systems/acid_base.py +324 -0
  178. chemistrykit/solutions/systems/activity.py +152 -0
  179. chemistrykit/solutions/systems/solubility.py +155 -0
  180. chemistrykit/solutions/systems/titration.py +219 -0
  181. chemistrykit/solutions/tests/__init__.py +0 -0
  182. chemistrykit/solutions/tests/test_acid_base.py +93 -0
  183. chemistrykit/solutions/tests/test_activity.py +61 -0
  184. chemistrykit/solutions/tests/test_solubility.py +68 -0
  185. chemistrykit/solutions/tests/test_titration.py +86 -0
  186. chemistrykit/solutions/tests/test_visualizers.py +38 -0
  187. chemistrykit/solutions/utils/__init__.py +0 -0
  188. chemistrykit/solutions/utils/rootfinding.py +117 -0
  189. chemistrykit/solutions/visualizers/__init__.py +0 -0
  190. chemistrykit/solutions/visualizers/solutions_plots.py +93 -0
  191. chemistrykit/spectro/__init__.py +70 -0
  192. chemistrykit/spectro/core/__init__.py +0 -0
  193. chemistrykit/spectro/core/base_system.py +106 -0
  194. chemistrykit/spectro/systems/__init__.py +0 -0
  195. chemistrykit/spectro/systems/beer_lambert.py +174 -0
  196. chemistrykit/spectro/systems/electronic.py +183 -0
  197. chemistrykit/spectro/systems/nmr.py +197 -0
  198. chemistrykit/spectro/systems/rotational.py +192 -0
  199. chemistrykit/spectro/systems/vibrational.py +419 -0
  200. chemistrykit/spectro/tests/__init__.py +0 -0
  201. chemistrykit/spectro/tests/test_beer_lambert.py +66 -0
  202. chemistrykit/spectro/tests/test_electronic.py +71 -0
  203. chemistrykit/spectro/tests/test_lineshapes.py +75 -0
  204. chemistrykit/spectro/tests/test_nmr.py +73 -0
  205. chemistrykit/spectro/tests/test_rotational.py +72 -0
  206. chemistrykit/spectro/tests/test_spectrum.py +31 -0
  207. chemistrykit/spectro/tests/test_vibrational.py +122 -0
  208. chemistrykit/spectro/tests/test_visualizers.py +40 -0
  209. chemistrykit/spectro/utils/__init__.py +0 -0
  210. chemistrykit/spectro/utils/lineshapes.py +214 -0
  211. chemistrykit/spectro/visualizers/__init__.py +0 -0
  212. chemistrykit/spectro/visualizers/spectro_plots.py +101 -0
  213. chemistrykit/statmech/__init__.py +51 -0
  214. chemistrykit/statmech/core/__init__.py +1 -0
  215. chemistrykit/statmech/core/base_system.py +151 -0
  216. chemistrykit/statmech/systems/__init__.py +3 -0
  217. chemistrykit/statmech/systems/lattice_gas.py +151 -0
  218. chemistrykit/statmech/systems/maxwell_boltzmann.py +211 -0
  219. chemistrykit/statmech/systems/partition_functions.py +371 -0
  220. chemistrykit/statmech/tests/__init__.py +0 -0
  221. chemistrykit/statmech/tests/test_lattice_gas.py +58 -0
  222. chemistrykit/statmech/tests/test_maxwell_boltzmann.py +95 -0
  223. chemistrykit/statmech/tests/test_partition_functions.py +137 -0
  224. chemistrykit/statmech/tests/test_visualizers.py +38 -0
  225. chemistrykit/statmech/utils/__init__.py +2 -0
  226. chemistrykit/statmech/utils/combinatorics.py +78 -0
  227. chemistrykit/statmech/utils/thermal_wavelength.py +53 -0
  228. chemistrykit/statmech/visualizers/__init__.py +2 -0
  229. chemistrykit/statmech/visualizers/statmech_plots.py +110 -0
  230. chemistrykit/structure/__init__.py +49 -0
  231. chemistrykit/structure/core/__init__.py +0 -0
  232. chemistrykit/structure/core/base_system.py +278 -0
  233. chemistrykit/structure/systems/__init__.py +0 -0
  234. chemistrykit/structure/systems/bonding.py +193 -0
  235. chemistrykit/structure/systems/lewis.py +180 -0
  236. chemistrykit/structure/systems/point_group.py +498 -0
  237. chemistrykit/structure/systems/vsepr.py +296 -0
  238. chemistrykit/structure/tests/__init__.py +0 -0
  239. chemistrykit/structure/tests/test_bonding.py +65 -0
  240. chemistrykit/structure/tests/test_lewis.py +62 -0
  241. chemistrykit/structure/tests/test_molecule.py +89 -0
  242. chemistrykit/structure/tests/test_point_group.py +165 -0
  243. chemistrykit/structure/tests/test_visualizers.py +33 -0
  244. chemistrykit/structure/tests/test_vsepr.py +103 -0
  245. chemistrykit/structure/utils/__init__.py +0 -0
  246. chemistrykit/structure/utils/symmetry_ops.py +243 -0
  247. chemistrykit/structure/visualizers/__init__.py +0 -0
  248. chemistrykit/structure/visualizers/structure_plots.py +124 -0
  249. chemistrykit/surface/__init__.py +45 -0
  250. chemistrykit/surface/core/__init__.py +0 -0
  251. chemistrykit/surface/core/base_system.py +83 -0
  252. chemistrykit/surface/systems/__init__.py +0 -0
  253. chemistrykit/surface/systems/bet.py +220 -0
  254. chemistrykit/surface/systems/catalysis.py +169 -0
  255. chemistrykit/surface/systems/freundlich.py +175 -0
  256. chemistrykit/surface/systems/langmuir.py +195 -0
  257. chemistrykit/surface/systems/langmuir_hinshelwood.py +145 -0
  258. chemistrykit/surface/tests/__init__.py +0 -0
  259. chemistrykit/surface/tests/test_bet.py +65 -0
  260. chemistrykit/surface/tests/test_catalysis.py +35 -0
  261. chemistrykit/surface/tests/test_freundlich.py +45 -0
  262. chemistrykit/surface/tests/test_langmuir.py +61 -0
  263. chemistrykit/surface/tests/test_langmuir_hinshelwood.py +49 -0
  264. chemistrykit/surface/tests/test_visualizers.py +40 -0
  265. chemistrykit/surface/utils/__init__.py +0 -0
  266. chemistrykit/surface/utils/regression.py +57 -0
  267. chemistrykit/surface/visualizers/__init__.py +0 -0
  268. chemistrykit/surface/visualizers/surface_plots.py +104 -0
  269. chemistrykit/tests/__init__.py +0 -0
  270. chemistrykit/tests/test_package.py +62 -0
  271. chemistrykit/thermo/__init__.py +61 -0
  272. chemistrykit/thermo/core/__init__.py +0 -0
  273. chemistrykit/thermo/core/base_system.py +106 -0
  274. chemistrykit/thermo/systems/__init__.py +0 -0
  275. chemistrykit/thermo/systems/equations_of_state.py +303 -0
  276. chemistrykit/thermo/systems/equilibrium.py +433 -0
  277. chemistrykit/thermo/systems/mixtures.py +273 -0
  278. chemistrykit/thermo/systems/phase_equilibria.py +187 -0
  279. chemistrykit/thermo/tests/__init__.py +0 -0
  280. chemistrykit/thermo/tests/test_equations_of_state.py +95 -0
  281. chemistrykit/thermo/tests/test_equilibrium.py +112 -0
  282. chemistrykit/thermo/tests/test_mixtures.py +68 -0
  283. chemistrykit/thermo/tests/test_phase_equilibria.py +56 -0
  284. chemistrykit/thermo/tests/test_visualizers.py +53 -0
  285. chemistrykit/thermo/utils/__init__.py +0 -0
  286. chemistrykit/thermo/utils/cubic_roots.py +58 -0
  287. chemistrykit/thermo/utils/regression.py +57 -0
  288. chemistrykit/thermo/visualizers/__init__.py +0 -0
  289. chemistrykit/thermo/visualizers/thermo_plots.py +157 -0
  290. chemistrykit-0.1.0.dist-info/METADATA +200 -0
  291. chemistrykit-0.1.0.dist-info/RECORD +294 -0
  292. chemistrykit-0.1.0.dist-info/WHEEL +5 -0
  293. chemistrykit-0.1.0.dist-info/licenses/LICENSE +21 -0
  294. chemistrykit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,69 @@
1
+ """chemistrykit: unified numerical toolkit for computational chemistry.
2
+
3
+ Import as ``ck`` by convention::
4
+
5
+ import chemistrykit as ck
6
+ ck.kinetics.FirstOrder(k=0.1, C0=1.0)
7
+ ck.kinetics.StoichiometricNetwork.consecutive(k1=1.0, k2=0.3)
8
+ ck.thermo.VanDerWaals(a=0.1448, b=3.913e-5)
9
+ ck.solutions.WeakAcid(Ca=0.1, Ka=1.8e-5).pH()
10
+ ck.md.LJFluid.from_lattice(n_per_side=4, density=0.6, temperature=1.0)
11
+ ck.statmech.MaxwellBoltzmannSpeedDistribution(mass=6.63e-26, temperature=298.15)
12
+ ck.structure.determine_point_group(water_molecule)
13
+ ck.spectro.rotational_spectrum(rotor, J_max=10, temperature=300.0)
14
+ ck.electrochem.nernst_potential(E_standard=0.34, n=2, Q=0.01)
15
+ ck.photochem.jablonski_network(kf=2.0, kic=1.0, kisc=0.5, kp=0.3, kic_T=0.2)
16
+ ck.surface.LangmuirIsotherm(K=2.0, qmax=5.0).loading(P=1.0)
17
+ ck.polymer.IdealChain().end_to_end_distance(n=1000, b=0.5)
18
+ ck.crystal.FaceCenteredCubicPacking().packing_fraction()
19
+ ck.analytical.fit_calibration([0, 1, 2, 3], [0.1, 1.0, 2.1, 2.9]).lod()
20
+ ck.constants.R
21
+ ck.integrators.rk4_integrate(...)
22
+
23
+ chemistrykit mirrors the architecture of the sibling project physicskit
24
+ (pk): one subpackage per chemistry domain, sharing common ODE integrators
25
+ (:mod:`chemistrykit.integrators`) and physical/chemical constants
26
+ (:mod:`chemistrykit.constants`). All 14 domains from
27
+ ``chemistrykit-spec.md``'s build plan are implemented; see ``__all__``
28
+ below for the full list.
29
+ """
30
+
31
+ from chemistrykit import (
32
+ analytical,
33
+ constants,
34
+ crystal,
35
+ electrochem,
36
+ integrators,
37
+ kinetics,
38
+ md,
39
+ photochem,
40
+ polymer,
41
+ quantum,
42
+ solutions,
43
+ spectro,
44
+ statmech,
45
+ structure,
46
+ surface,
47
+ thermo,
48
+ )
49
+
50
+ __version__ = "0.1.0"
51
+
52
+ __all__ = [
53
+ "constants",
54
+ "integrators",
55
+ "kinetics",
56
+ "thermo",
57
+ "solutions",
58
+ "md",
59
+ "statmech",
60
+ "quantum",
61
+ "spectro",
62
+ "structure",
63
+ "electrochem",
64
+ "photochem",
65
+ "surface",
66
+ "polymer",
67
+ "crystal",
68
+ "analytical",
69
+ ]
@@ -0,0 +1,61 @@
1
+ """chemistrykit.analytical: analytical chemistry.
2
+
3
+ Acid-base titration curves are provided by
4
+ :mod:`chemistrykit.solutions.systems.titration`; this subpackage adds
5
+ redox and complexometric (EDTA) titration-curve simulation with
6
+ equivalence-point detection; chromatographic plate theory and the van
7
+ Deemter equation (resolution, selectivity); linear-regression calibration
8
+ curves with IUPAC-convention limits of detection/quantitation; and
9
+ propagation-of-uncertainty formulas plus Dixon's Q-test for outlier
10
+ rejection.
11
+ """
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ from chemistrykit.analytical.core.base_system import TitrationCurve, TitrationCurveResult
16
+ from chemistrykit.analytical.systems.calibration import LinearCalibration, fit_calibration
17
+ from chemistrykit.analytical.systems.chromatography import (
18
+ minimum_plate_height,
19
+ optimum_flow_velocity,
20
+ plate_height,
21
+ resolution,
22
+ retention_factor,
23
+ selectivity_factor,
24
+ simulate_chromatogram,
25
+ theoretical_plates,
26
+ van_deemter_H,
27
+ )
28
+ from chemistrykit.analytical.systems.qtest import Q_CRITICAL_TABLE, QTestResult, dixon_q_test
29
+ from chemistrykit.analytical.systems.titration import EDTATitration, RedoxTitration
30
+ from chemistrykit.analytical.systems.uncertainty import (
31
+ propagate_power,
32
+ propagate_product,
33
+ propagate_sum,
34
+ propagate_uncertainty,
35
+ )
36
+
37
+ __all__ = [
38
+ "__version__",
39
+ "TitrationCurve",
40
+ "TitrationCurveResult",
41
+ "RedoxTitration",
42
+ "EDTATitration",
43
+ "theoretical_plates",
44
+ "plate_height",
45
+ "van_deemter_H",
46
+ "optimum_flow_velocity",
47
+ "minimum_plate_height",
48
+ "retention_factor",
49
+ "selectivity_factor",
50
+ "resolution",
51
+ "simulate_chromatogram",
52
+ "LinearCalibration",
53
+ "fit_calibration",
54
+ "propagate_sum",
55
+ "propagate_product",
56
+ "propagate_power",
57
+ "propagate_uncertainty",
58
+ "Q_CRITICAL_TABLE",
59
+ "QTestResult",
60
+ "dixon_q_test",
61
+ ]
File without changes
@@ -0,0 +1,118 @@
1
+ r"""Abstract base class for potentiometric-style titration-curve models, and a note on scope.
2
+
3
+ :class:`TitrationCurve` captures the one genuinely polymorphic shape in
4
+ this domain: several titration *types* (redox, complexometric --
5
+ :mod:`chemistrykit.analytical.systems.titration`) that all reduce to
6
+ "compute some scalar response as a function of titrant volume, then find
7
+ the equivalence point as the point of steepest response change" -- the
8
+ same shape that :class:`chemistrykit.solutions.core.base_system.Titration`
9
+ already captures for acid-base titrations (pH vs. volume).
10
+
11
+ **Why a new ABC here, rather than reusing** :class:`chemistrykit.solutions.core.base_system.Titration`:
12
+ that class's abstract method is named and documented specifically as
13
+ ``pH_at`` (a *hydrogen-ion* activity readout), which is the wrong
14
+ abstraction for a redox titration's electrode potential `E` or a
15
+ complexometric titration's `pM`. Rather than force those into a
16
+ pH-flavored interface (or reach across a domain boundary the way
17
+ ``chemistrykit.photochem`` reaches into ``chemistrykit.kinetics`` for
18
+ substantial shared machinery -- not warranted here, since the shared
19
+ logic is a handful of lines), :class:`TitrationCurve` reimplements the
20
+ same small pattern (curve, steepest-ascent-or-descent equivalence-point
21
+ detection) with a response-type-neutral abstract method name, exactly
22
+ the way :mod:`chemistrykit.surface.utils.regression`,
23
+ :mod:`chemistrykit.electrochem.utils.regression`, and
24
+ :mod:`chemistrykit.photochem.utils.regression` each keep their own small
25
+ ``linear_fit`` rather than importing one another's. Acid-base titration
26
+ curves are *not* reimplemented here at all -- :mod:`chemistrykit.analytical`'s
27
+ examples and tests use
28
+ :mod:`chemistrykit.solutions.systems.titration`'s classes directly for
29
+ that case, side by side with the new redox/complexometric models below.
30
+
31
+ Chromatography (:mod:`chemistrykit.analytical.systems.chromatography`),
32
+ calibration curves (:mod:`chemistrykit.analytical.systems.calibration`),
33
+ uncertainty propagation (:mod:`chemistrykit.analytical.systems.uncertainty`),
34
+ and the Q-test (:mod:`chemistrykit.analytical.systems.qtest`) are each a
35
+ self-contained set of formulas with no swappable sibling, so -- following
36
+ ``chemistrykit.electrochem``/``chemistrykit.photochem``/``chemistrykit.surface``'s
37
+ precedent -- they stay as plain functions (plus small result dataclasses)
38
+ in their own ``systems/`` modules.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ from abc import ABC, abstractmethod
44
+ from dataclasses import dataclass
45
+
46
+ import numpy as np
47
+
48
+ __all__ = ["TitrationCurveResult", "TitrationCurve"]
49
+
50
+
51
+ @dataclass
52
+ class TitrationCurveResult:
53
+ """Container for the output of a :meth:`TitrationCurve.curve` call."""
54
+
55
+ V: np.ndarray
56
+ """ndarray: Volume(s) of titrant added, in L (or any consistent volume unit)."""
57
+
58
+ response: np.ndarray
59
+ """ndarray: The titration's response variable at each volume in `V`
60
+ (e.g. electrode potential `E` in volts, or `pM`)."""
61
+
62
+
63
+ class TitrationCurve(ABC):
64
+ """Common base for a potentiometric titration-curve model whose response is not pH.
65
+
66
+ Concrete subclasses implement :meth:`response_at`; :meth:`curve` and
67
+ :meth:`find_equivalence_point` are then available for free, mirroring
68
+ :meth:`chemistrykit.solutions.core.base_system.Titration.curve`/
69
+ :meth:`~chemistrykit.solutions.core.base_system.Titration.find_equivalence_point`.
70
+ """
71
+
72
+ @abstractmethod
73
+ def response_at(self, V: np.ndarray) -> np.ndarray:
74
+ """Return the titration's response variable at each titrant volume in `V`.
75
+
76
+ Parameters
77
+ ----------
78
+ V : ndarray
79
+ Volume(s) of titrant added, in L.
80
+
81
+ Returns
82
+ -------
83
+ ndarray
84
+ """
85
+
86
+ def curve(self, V) -> TitrationCurveResult:
87
+ """Compute the full titration curve over a range of titrant volumes.
88
+
89
+ Parameters
90
+ ----------
91
+ V : array-like of float
92
+ Volumes of titrant added, in L.
93
+
94
+ Returns
95
+ -------
96
+ TitrationCurveResult
97
+ """
98
+ V = np.atleast_1d(np.asarray(V, dtype=np.float64))
99
+ response = np.asarray(self.response_at(V), dtype=np.float64)
100
+ return TitrationCurveResult(V=V, response=response)
101
+
102
+ def find_equivalence_point(self, V) -> float:
103
+ r"""Numerically locate the equivalence point as the point of steepest response change.
104
+
105
+ Parameters
106
+ ----------
107
+ V : array-like of float
108
+ A sufficiently fine grid of titrant volumes spanning the
109
+ equivalence point, in L.
110
+
111
+ Returns
112
+ -------
113
+ float
114
+ """
115
+ result = self.curve(V)
116
+ d_response_dV = np.gradient(result.response, result.V)
117
+ idx = int(np.argmax(np.abs(d_response_dV)))
118
+ return float(result.V[idx])
File without changes
@@ -0,0 +1,131 @@
1
+ r"""Linear-regression calibration curves, and IUPAC-convention limits of detection/quantitation.
2
+
3
+ See Harris, *Quantitative Chemical Analysis*, 9th ed., Ch. 4.5 ("Method
4
+ Validation") and Ch. 5, or the IUPAC recommendation (G. L. Long & J. D.
5
+ Winefordner, *Anal. Chem.* 55, 712A (1983)), for the standard :math:`3.3
6
+ \sigma/m` (LOD) and :math:`10\sigma/m` (LOQ) convention, with `sigma` the
7
+ calibration curve's residual standard error and `m` its slope.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+
14
+ import numpy as np
15
+
16
+ from chemistrykit.analytical.utils.regression import linear_fit
17
+
18
+ __all__ = ["LinearCalibration", "fit_calibration"]
19
+
20
+ #: float: IUPAC LOD multiplier (Long & Winefordner, 1983).
21
+ _LOD_MULTIPLIER = 3.3
22
+
23
+ #: float: IUPAC LOQ multiplier (Long & Winefordner, 1983).
24
+ _LOQ_MULTIPLIER = 10.0
25
+
26
+
27
+ @dataclass
28
+ class LinearCalibration:
29
+ """A fitted instrument-response-vs-concentration calibration curve, ``signal = slope*conc + intercept``."""
30
+
31
+ slope: float
32
+ """float: Sensitivity, signal units per concentration unit."""
33
+
34
+ intercept: float
35
+ """float: Signal at zero concentration (ideally the blank signal)."""
36
+
37
+ r_squared: float
38
+ """float: Coefficient of determination of the fit."""
39
+
40
+ residual_std_error: float
41
+ """float: :math:`s_{y/x}`, the residual standard error about the fit
42
+ (Harris, *Quantitative Chemical Analysis*, 9th ed., Ch. 4.5)."""
43
+
44
+ def predict_signal(self, concentration):
45
+ """Predict the instrument signal at given concentration(s).
46
+
47
+ Parameters
48
+ ----------
49
+ concentration : float or array-like of float
50
+
51
+ Returns
52
+ -------
53
+ float or ndarray
54
+ """
55
+ c = np.asarray(concentration, dtype=np.float64)
56
+ result = self.slope * c + self.intercept
57
+ return float(result) if result.ndim == 0 else result
58
+
59
+ def predict_concentration(self, signal):
60
+ r"""Invert the calibration to estimate concentration from a measured signal.
61
+
62
+ .. math::
63
+
64
+ \hat c = (y_{measured} - b)/m
65
+
66
+ Parameters
67
+ ----------
68
+ signal : float or array-like of float
69
+
70
+ Returns
71
+ -------
72
+ float or ndarray
73
+ """
74
+ y = np.asarray(signal, dtype=np.float64)
75
+ result = (y - self.intercept) / self.slope
76
+ return float(result) if result.ndim == 0 else result
77
+
78
+ def lod(self) -> float:
79
+ r"""Limit of detection, :math:`\text{LOD}=3.3\,s_{y/x}/|m|` (IUPAC convention).
80
+
81
+ Returns
82
+ -------
83
+ float
84
+ """
85
+ return _LOD_MULTIPLIER * self.residual_std_error / abs(self.slope)
86
+
87
+ def loq(self) -> float:
88
+ r"""Limit of quantitation, :math:`\text{LOQ}=10\,s_{y/x}/|m|` (IUPAC convention).
89
+
90
+ Returns
91
+ -------
92
+ float
93
+ """
94
+ return _LOQ_MULTIPLIER * self.residual_std_error / abs(self.slope)
95
+
96
+
97
+ def fit_calibration(concentration, signal) -> LinearCalibration:
98
+ r"""Fit a linear calibration curve (signal vs. concentration) by ordinary least squares.
99
+
100
+ Parameters
101
+ ----------
102
+ concentration : array-like of float
103
+ Known standard concentrations (at least 3 distinct values, so the
104
+ residual standard error is defined).
105
+ signal : array-like of float
106
+ Corresponding measured instrument signals.
107
+
108
+ Returns
109
+ -------
110
+ LinearCalibration
111
+
112
+ Examples
113
+ --------
114
+ LOQ is always exactly :math:`10/3.3` times LOD, by definition,
115
+ regardless of the data:
116
+
117
+ >>> conc = [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]
118
+ >>> signal = [0.02, 1.05, 1.98, 3.10, 3.95, 5.08]
119
+ >>> cal = fit_calibration(conc, signal)
120
+ >>> round(cal.loq() / cal.lod(), 6) == round(10.0 / 3.3, 6)
121
+ True
122
+
123
+ A perfect (noiseless) calibration has zero residual error and hence
124
+ zero LOD/LOQ:
125
+
126
+ >>> cal_perfect = fit_calibration([0.0, 1.0, 2.0, 3.0], [1.0, 3.0, 5.0, 7.0])
127
+ >>> cal_perfect.lod() < 1e-9
128
+ True
129
+ """
130
+ fit = linear_fit(concentration, signal)
131
+ return LinearCalibration(slope=fit.slope, intercept=fit.intercept, r_squared=fit.r_squared, residual_std_error=fit.residual_std_error)
@@ -0,0 +1,317 @@
1
+ r"""Chromatographic plate theory: theoretical plates, the van Deemter equation, resolution, and selectivity.
2
+
3
+ See Harris, *Quantitative Chemical Analysis*, 9th ed., Ch. 23 ("An
4
+ Introduction to Chromatographic Separations"), or Skoog, West, Holler &
5
+ Crouch, *Fundamentals of Analytical Chemistry*, 9th ed., Ch. 26,
6
+ throughout.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import numpy as np
12
+
13
+ from chemistrykit.spectro.utils.lineshapes import gaussian
14
+
15
+ __all__ = [
16
+ "theoretical_plates",
17
+ "plate_height",
18
+ "van_deemter_H",
19
+ "optimum_flow_velocity",
20
+ "minimum_plate_height",
21
+ "retention_factor",
22
+ "selectivity_factor",
23
+ "resolution",
24
+ "simulate_chromatogram",
25
+ ]
26
+
27
+
28
+ def theoretical_plates(retention_time: float, peak_width: float, width_type: str = "base"):
29
+ r"""Number of theoretical plates `N` from a peak's retention time and width.
30
+
31
+ For a Gaussian elution peak (Harris, *Quantitative Chemical
32
+ Analysis*, 9th ed., eq. 23.19-23.20):
33
+
34
+ .. math::
35
+
36
+ N = 16\left(\frac{t_R}{w_{base}}\right)^2 = 5.545\left(\frac{t_R}{w_{1/2}}\right)^2
37
+
38
+ where :math:`w_{base}` is the width at the peak base (tangents to the
39
+ inflection points) and :math:`w_{1/2}` is the full width at half
40
+ maximum -- the numeric prefactor differs only because a Gaussian's
41
+ base width (4 standard deviations) and FWHM
42
+ (:math:`2\sqrt{2\ln2}\,\sigma`) are different multiples of `sigma`.
43
+
44
+ Parameters
45
+ ----------
46
+ retention_time : float or array-like of float
47
+ Peak retention time :math:`t_R`.
48
+ peak_width : float or array-like of float
49
+ Peak width, in the same units as `retention_time`, of the type
50
+ selected by `width_type`.
51
+ width_type : {"base", "half_height"}, default "base"
52
+
53
+ Returns
54
+ -------
55
+ float or ndarray
56
+
57
+ Examples
58
+ --------
59
+ A peak eluting at 10.0 min with a 0.5 min base width:
60
+
61
+ >>> round(float(theoretical_plates(10.0, 0.5, width_type="base")), 1)
62
+ 6400.0
63
+
64
+ The two width conventions must agree for a Gaussian peak of
65
+ consistent shape (base width = 4 sigma, FWHM =
66
+ :math:`2\sqrt{2\ln2}\,\sigma\approx2.3548\sigma`):
67
+
68
+ >>> sigma = 0.125
69
+ >>> N_base = theoretical_plates(10.0, 4.0 * sigma, width_type="base")
70
+ >>> N_half = theoretical_plates(10.0, 2.3548 * sigma, width_type="half_height")
71
+ >>> bool(abs(N_base - N_half) / N_base < 1e-3)
72
+ True
73
+ """
74
+ tR = np.asarray(retention_time, dtype=np.float64)
75
+ w = np.asarray(peak_width, dtype=np.float64)
76
+ if width_type == "base":
77
+ result = 16.0 * (tR / w) ** 2
78
+ elif width_type == "half_height":
79
+ result = 5.545 * (tR / w) ** 2
80
+ else:
81
+ raise ValueError('width_type must be "base" or "half_height"')
82
+ return float(result) if result.ndim == 0 else result
83
+
84
+
85
+ def plate_height(column_length: float, N):
86
+ r"""Plate height (HETP) :math:`H=L/N`, the column length per theoretical plate.
87
+
88
+ Parameters
89
+ ----------
90
+ column_length : float
91
+ Column length `L`, e.g. in cm.
92
+ N : float or array-like of float
93
+ Number of theoretical plates.
94
+
95
+ Returns
96
+ -------
97
+ float or ndarray
98
+
99
+ Examples
100
+ --------
101
+ >>> round(float(plate_height(column_length=25.0, N=6400.0)), 6)
102
+ 0.003906
103
+ """
104
+ N = np.asarray(N, dtype=np.float64)
105
+ result = column_length / N
106
+ return float(result) if result.ndim == 0 else result
107
+
108
+
109
+ def van_deemter_H(u, A: float, B: float, C: float):
110
+ r"""The van Deemter equation: plate height `H` as a function of mobile-phase linear velocity `u`.
111
+
112
+ .. math::
113
+
114
+ H = A + \frac{B}{u} + Cu
115
+
116
+ with `A` the eddy-diffusion term (velocity-independent band
117
+ broadening from unequal flow paths), `B` the longitudinal-molecular-
118
+ diffusion term (dominant at low `u`), and `C` the mass-transfer-
119
+ resistance term (dominant at high `u`) (J. J. van Deemter, F. J.
120
+ Zuiderweg, A. Klinkenberg, *Chem. Eng. Sci.* 5, 271 (1956); Harris,
121
+ *Quantitative Chemical Analysis*, 9th ed., Ch. 23.4).
122
+
123
+ Parameters
124
+ ----------
125
+ u : float or array-like of float
126
+ Mobile-phase linear velocity.
127
+ A, B, C : float
128
+ Van Deemter coefficients (all non-negative for a physical column).
129
+
130
+ Returns
131
+ -------
132
+ float or ndarray
133
+
134
+ Examples
135
+ --------
136
+ >>> round(float(van_deemter_H(u=2.0, A=1.0, B=2.0, C=0.05)), 4)
137
+ 2.1
138
+ """
139
+ u = np.asarray(u, dtype=np.float64)
140
+ result = A + B / u + C * u
141
+ return float(result) if result.ndim == 0 else result
142
+
143
+
144
+ def optimum_flow_velocity(B: float, C: float) -> float:
145
+ r"""The flow velocity :math:`u_{opt}=\sqrt{B/C}` minimizing the van Deemter equation.
146
+
147
+ Found by :math:`dH/du=-B/u^2+C=0` (Harris, *Quantitative Chemical
148
+ Analysis*, 9th ed., Ch. 23.4).
149
+
150
+ Parameters
151
+ ----------
152
+ B, C : float
153
+ Van Deemter longitudinal-diffusion and mass-transfer coefficients.
154
+
155
+ Returns
156
+ -------
157
+ float
158
+
159
+ Examples
160
+ --------
161
+ >>> round(optimum_flow_velocity(B=2.0, C=0.05), 6)
162
+ 6.324555
163
+ """
164
+ return float(np.sqrt(B / C))
165
+
166
+
167
+ def minimum_plate_height(A: float, B: float, C: float) -> float:
168
+ r"""The minimum plate height :math:`H_{min}=A+2\sqrt{BC}`, at the optimum flow velocity.
169
+
170
+ Substituting :func:`optimum_flow_velocity` into
171
+ :func:`van_deemter_H` (Harris, *Quantitative Chemical Analysis*, 9th
172
+ ed., Ch. 23.4).
173
+
174
+ Parameters
175
+ ----------
176
+ A, B, C : float
177
+
178
+ Returns
179
+ -------
180
+ float
181
+
182
+ Examples
183
+ --------
184
+ Matches direct evaluation of :func:`van_deemter_H` at the optimum
185
+ velocity from :func:`optimum_flow_velocity`:
186
+
187
+ >>> A, B, C = 1.0, 2.0, 0.05
188
+ >>> u_opt = optimum_flow_velocity(B, C)
189
+ >>> bool(round(minimum_plate_height(A, B, C), 6) == round(float(van_deemter_H(u_opt, A, B, C)), 6))
190
+ True
191
+ """
192
+ return A + 2.0 * np.sqrt(B * C)
193
+
194
+
195
+ def retention_factor(retention_time: float, dead_time: float):
196
+ r"""The retention (capacity) factor :math:`k=(t_R-t_0)/t_0`.
197
+
198
+ Parameters
199
+ ----------
200
+ retention_time : float or array-like of float
201
+ Analyte retention time :math:`t_R`.
202
+ dead_time : float
203
+ Column dead time (retention time of an unretained species) :math:`t_0`.
204
+
205
+ Returns
206
+ -------
207
+ float or ndarray
208
+
209
+ Examples
210
+ --------
211
+ >>> round(float(retention_factor(retention_time=12.0, dead_time=2.0)), 6)
212
+ 5.0
213
+ """
214
+ tR = np.asarray(retention_time, dtype=np.float64)
215
+ result = (tR - dead_time) / dead_time
216
+ return float(result) if result.ndim == 0 else result
217
+
218
+
219
+ def selectivity_factor(k1: float, k2: float) -> float:
220
+ r"""The selectivity (relative retention) factor :math:`\alpha=k_2/k_1` (:math:`k_2\ge k_1` by convention).
221
+
222
+ Parameters
223
+ ----------
224
+ k1, k2 : float
225
+ Retention factors of the earlier- and later-eluting peaks.
226
+
227
+ Returns
228
+ -------
229
+ float
230
+
231
+ Examples
232
+ --------
233
+ >>> round(selectivity_factor(k1=2.0, k2=5.0), 6)
234
+ 2.5
235
+ """
236
+ return k2 / k1
237
+
238
+
239
+ def resolution(tR1: float, tR2: float, w1: float, w2: float) -> float:
240
+ r"""Chromatographic resolution :math:`R_s=2(t_{R,2}-t_{R,1})/(w_1+w_2)` between two adjacent peaks.
241
+
242
+ :math:`R_s\ge1.5` is the conventional criterion for baseline
243
+ separation (Harris, *Quantitative Chemical Analysis*, 9th ed., eq.
244
+ 23.21).
245
+
246
+ Parameters
247
+ ----------
248
+ tR1, tR2 : float
249
+ Retention times of the earlier- and later-eluting peaks.
250
+ w1, w2 : float
251
+ Base widths of the two peaks, same time units.
252
+
253
+ Returns
254
+ -------
255
+ float
256
+
257
+ Examples
258
+ --------
259
+ >>> round(resolution(tR1=9.0, tR2=10.0, w1=0.5, w2=0.5), 6)
260
+ 2.0
261
+ """
262
+ return 2.0 * (tR2 - tR1) / (w1 + w2)
263
+
264
+
265
+ def simulate_chromatogram(t, centers, retention_times=None, N: float = 10000.0, amplitudes=None):
266
+ r"""Simulate a chromatogram as a sum of Gaussian elution peaks of plate-count-consistent width.
267
+
268
+ Reuses :func:`chemistrykit.spectro.utils.lineshapes.gaussian` (each
269
+ elution peak is, to a good approximation, Gaussian -- the same
270
+ plate-theory result that underlies :func:`theoretical_plates`), with
271
+ each peak's FWHM set by :math:`w_{1/2}=t_R\sqrt{8\ln2/N}` (inverting
272
+ :func:`theoretical_plates`'s half-height form,
273
+ :math:`N=5.545(t_R/w_{1/2})^2` with :math:`5.545\approx8\ln2`).
274
+
275
+ Parameters
276
+ ----------
277
+ t : array-like of float
278
+ Time grid to evaluate the chromatogram on.
279
+ centers : array-like of float
280
+ Retention time of each peak (alias for `retention_times`, kept
281
+ for a natural call signature with :func:`chemistrykit.spectro.utils.lineshapes.broaden_stick_spectrum`).
282
+ retention_times : array-like of float, optional
283
+ If given, overrides `centers`.
284
+ N : float, default 10000.0
285
+ Number of theoretical plates (assumed equal for every peak, the
286
+ common simplifying assumption for a single column/method).
287
+ amplitudes : array-like of float, optional
288
+ Relative peak heights; defaults to 1.0 for every peak.
289
+
290
+ Returns
291
+ -------
292
+ ndarray, shape matching `t`
293
+
294
+ Examples
295
+ --------
296
+ A single simulated peak's apparent plate count (recovered from its
297
+ numerically-measured FWHM) matches the `N` it was built from:
298
+
299
+ >>> import numpy as np
300
+ >>> t = np.linspace(8.0, 12.0, 200001)
301
+ >>> N_true = 10000.0
302
+ >>> chrom = simulate_chromatogram(t, centers=[10.0], N=N_true)
303
+ >>> half_max = chrom.max() / 2.0
304
+ >>> above = t[chrom >= half_max]
305
+ >>> fwhm = above.max() - above.min()
306
+ >>> N_recovered = theoretical_plates(10.0, fwhm, width_type="half_height")
307
+ >>> bool(abs(N_recovered - N_true) / N_true < 0.01)
308
+ True
309
+ """
310
+ t = np.asarray(t, dtype=np.float64)
311
+ centers = np.asarray(retention_times if retention_times is not None else centers, dtype=np.float64)
312
+ amps = np.ones_like(centers) if amplitudes is None else np.asarray(amplitudes, dtype=np.float64)
313
+ chrom = np.zeros_like(t)
314
+ for tR, amp in zip(centers, amps, strict=True):
315
+ fwhm = tR * np.sqrt(8.0 * np.log(2.0) / N)
316
+ chrom += amp * gaussian(t, tR, fwhm)
317
+ return chrom