stjames 0.0.45__py3-none-any.whl → 0.0.47__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.

Potentially problematic release.


This version of stjames might be problematic. Click here for more details.

stjames/base.py CHANGED
@@ -12,7 +12,7 @@ class Base(pydantic.BaseModel):
12
12
  @classmethod
13
13
  def coerce_numpy(cls, val: _T) -> _T | list[Any]:
14
14
  if isinstance(val, np.ndarray):
15
- return val.tolist() # type: ignore [no-any-return, unused-ignore]
15
+ return val.tolist() # type: ignore [no-any-return, unused-ignore, return-value]
16
16
 
17
17
  return val
18
18
 
stjames/constraint.py CHANGED
@@ -11,6 +11,7 @@ class ConstraintType(LowercaseStrEnum):
11
11
  BOND = "bond"
12
12
  ANGLE = "angle"
13
13
  DIHEDRAL = "dihedral"
14
+ FREEZE_ATOMS = "freeze_atoms"
14
15
 
15
16
 
16
17
  class Constraint(Base):
@@ -18,7 +19,7 @@ class Constraint(Base):
18
19
  Represents a single (absolute) constraint.
19
20
 
20
21
  :param constraint_type: which type
21
- :param atoms: the atoms in question
22
+ :param atoms: the atoms in question. n.b. - these are 1-indexed!
22
23
  :param value: the value to constrain this to, leaving this blank sets the current value
23
24
  """
24
25
 
@@ -38,6 +39,9 @@ class Constraint(Base):
38
39
  case ConstraintType.DIHEDRAL:
39
40
  if len(self.atoms) != 4:
40
41
  raise ValueError("Dihedral constraint needs four atom indices!")
42
+ case ConstraintType.FREEZE_ATOMS:
43
+ if len(self.atoms) == 0:
44
+ raise ValueError("Can't freeze atoms without any atoms to freeze!")
41
45
  case _:
42
46
  raise ValueError("Unknown constraint_type!")
43
47
 
@@ -48,7 +52,7 @@ class PairwiseHarmonicConstraint(Base):
48
52
  """
49
53
  Represents a harmonic constraint, with a characteristic spring constant.
50
54
 
51
- :param atoms: whch atoms to apply to
55
+ :param atoms: which atoms to apply to
52
56
  :param force_constant: the strength of the attraction, in kcal/mol/Å
53
57
  :param equilibrium: the distance at which force is zero
54
58
  """
stjames/diis_settings.py CHANGED
@@ -4,7 +4,7 @@ from .base import Base, LowercaseStrEnum
4
4
 
5
5
 
6
6
  class DIISStrategy(LowercaseStrEnum):
7
- # regular pulay DIIS
7
+ # regular Pulay DIIS
8
8
  DIIS = "diis"
9
9
 
10
10
  # Hu/Yang JCP 2010 - ADIIS
stjames/method.py CHANGED
@@ -43,6 +43,8 @@ class Method(LowercaseStrEnum):
43
43
  # this was going to be removed, but Jonathon wrote such a nice basis set test... it's off the front end.
44
44
  BP86 = "bp86"
45
45
 
46
+ OFF_SAGE_2_2_1 = "off_sage_2_2_1"
47
+
46
48
 
47
49
  NNPMethod = Literal[Method.AIMNET2_WB97MD3]
48
50
  NNP_METHODS = [Method.AIMNET2_WB97MD3]
@@ -53,8 +55,11 @@ XTB_METHODS = [Method.GFN_FF, Method.GFN0_XTB, Method.GFN1_XTB, Method.GFN2_XTB]
53
55
  CompositeMethod = Literal[Method.HF3C, Method.B973C, Method.R2SCAN3C, Method.WB97X3C]
54
56
  COMPOSITE_METHODS = [Method.HF3C, Method.B973C, Method.R2SCAN3C, Method.WB97X3C]
55
57
 
56
- PrepackagedMethod = XTBMethod | CompositeMethod | NNPMethod
57
- PREPACKAGED_METHODS = [*XTB_METHODS, *COMPOSITE_METHODS]
58
+ FFMethod = Literal[Method.OFF_SAGE_2_2_1]
59
+ FF_METHODS = [Method.OFF_SAGE_2_2_1]
60
+
61
+ PrepackagedMethod = XTBMethod | CompositeMethod | NNPMethod | FFMethod
62
+ PREPACKAGED_METHODS = [*XTB_METHODS, *COMPOSITE_METHODS, *NNP_METHODS, *FF_METHODS]
58
63
 
59
64
  MethodWithCorrection = Literal[Method.WB97XD3, Method.WB97XV, Method.WB97MV, Method.WB97MD3BJ, Method.DSDBLYPD3BJ]
60
65
  METHODS_WITH_CORRECTION = [Method.WB97XD3, Method.WB97XV, Method.WB97MV, Method.WB97MD3BJ, Method.DSDBLYPD3BJ, Method.B97D3BJ]
stjames/scf_settings.py CHANGED
@@ -20,7 +20,7 @@ class SCFInitMethod(LowercaseStrEnum):
20
20
  class OrthonormalizationMethod(LowercaseStrEnum):
21
21
  SYMMETRIC = "symmetric"
22
22
  CANONICAL = "canonical"
23
- # cholesky, in future?
23
+ # Cholesky, in future?
24
24
 
25
25
 
26
26
  class SCFSettings(Base):
@@ -1,10 +1,15 @@
1
+ # ruff: noqa: F405
2
+ from typing import Literal
3
+
1
4
  from .admet import *
2
5
  from .basic_calculation import *
3
6
  from .bde import *
4
7
  from .conformer import *
5
8
  from .conformer_search import *
6
9
  from .descriptors import *
10
+ from .electronic_properties import *
7
11
  from .fukui import *
12
+ from .hydrogen_bond_basicity import *
8
13
  from .molecular_dynamics import *
9
14
  from .multistage_opt import *
10
15
  from .pka import *
@@ -12,3 +17,42 @@ from .redox_potential import *
12
17
  from .scan import *
13
18
  from .spin_states import *
14
19
  from .tautomer import *
20
+ from .workflow import Workflow
21
+
22
+ WORKFLOW_NAME = Literal[
23
+ "admet",
24
+ "basic_calculation",
25
+ "bde",
26
+ "conformers",
27
+ "conformer_search",
28
+ "descriptors",
29
+ "electronic_properties",
30
+ "fukui",
31
+ "hydrogen_bond_basicity",
32
+ "molecular_dynamics",
33
+ "multistage_opt",
34
+ "pka",
35
+ "redox_potential",
36
+ "scan",
37
+ "spin_states",
38
+ "tautomers",
39
+ ]
40
+
41
+ WORKFLOW_MAPPING: dict[str, Workflow] = {
42
+ "admet": ADMETWorkflow, # type: ignore [dict-item]
43
+ "basic_calculation": BasicCalculationWorkflow, # type: ignore [dict-item]
44
+ "bde": BDEWorkflow, # type: ignore [dict-item]
45
+ "conformers": ConformerWorkflow, # type: ignore [dict-item]
46
+ "conformer_search": ConformerSearchWorkflow, # type: ignore [dict-item]
47
+ "descriptors": DescriptorsWorkflow, # type: ignore [dict-item]
48
+ "electronic_properties": ElectronicPropertiesWorkflow, # type: ignore [dict-item]
49
+ "fukui": FukuiIndexWorkflow, # type: ignore [dict-item]
50
+ "hydrogen_bond_basicity": HydrogenBondBasicityWorkflow, # type: ignore [dict-item]
51
+ "molecular_dynamics": MolecularDynamicsWorkflow, # type: ignore [dict-item]
52
+ "multistage_opt": MultiStageOptWorkflow, # type: ignore [dict-item]
53
+ "pka": pKaWorkflow, # type: ignore [dict-item]
54
+ "redox_potential": RedoxPotentialWorkflow, # type: ignore [dict-item]
55
+ "scan": ScanWorkflow, # type: ignore [dict-item]
56
+ "spin_states": SpinStatesWorkflow, # type: ignore [dict-item]
57
+ "tautomers": TautomerWorkflow, # type: ignore [dict-item]
58
+ }
stjames/workflows/bde.py CHANGED
@@ -60,10 +60,10 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
60
60
  :param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
61
61
  :param solvent: solvent to use for singlepoint
62
62
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
63
+ :param frequencies: whether to calculate frequencies
63
64
 
64
65
  Overridden:
65
66
  :param mso_mode: Mode for MultiStageOptSettings
66
- :param frequencies: whether to calculate frequencies
67
67
 
68
68
  Turned off:
69
69
  :param constraints: constraints to add (not supported)
@@ -81,7 +81,6 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
81
81
  """
82
82
 
83
83
  mso_mode: Mode = _sentinel_mso_mode # type: ignore [assignment]
84
- frequencies: bool = False
85
84
  optimize_fragments: bool = None # type: ignore [assignment]
86
85
 
87
86
  atoms: tuple[PositiveInt, ...] = Field(default_factory=tuple)
@@ -9,6 +9,7 @@ from ..base import LowercaseStrEnum
9
9
  from ..constraint import Constraint
10
10
  from ..method import Method, XTBMethod
11
11
  from ..mode import Mode
12
+ from ..task import Task
12
13
  from ..types import UUID
13
14
  from .multistage_opt import MultiStageOptMixin
14
15
  from .workflow import Workflow
@@ -26,16 +27,16 @@ def check_sentinel(value: _T, default: _U) -> _T | _U:
26
27
 
27
28
  class ScreeningSettings(BaseModel):
28
29
  """
29
- Settings for determing unique and useful conformers.
30
+ Settings for determining unique and useful conformers.
30
31
 
31
- :param energy_threshhold: maximum relative energy for screening
32
- :param rotational_constants_threshhold: maximum difference in rotational constants for screening
33
- :param rmsd: cartesian RMSD for screening
32
+ :param energy_threshold: maximum relative energy for screening
33
+ :param rotational_constants_threshold: maximum difference in rotational constants for screening
34
+ :param rmsd: Cartesian RMSD for screening
34
35
  :param max_confs: maximum number of conformers to keep
35
36
  """
36
37
 
37
- energy_threshhold: float | None = None # kcal/mol
38
- rotational_constants_threshhold: float | None = 0.02
38
+ energy_threshold: float | None = None # kcal/mol
39
+ rotational_constants_threshold: float | None = 0.02
39
40
  rmsd: float | None = 0.25
40
41
  max_confs: int | None = None
41
42
 
@@ -76,10 +77,11 @@ class ETKDGSettings(ConformerGenSettings):
76
77
 
77
78
  Inherited:
78
79
  :param mode: Mode for calculations
80
+ :param conf_opt_method: method for the optimization
79
81
  :param screening: post-generation screening settings
80
82
  :param constraints: constraints for conformer generation
81
83
  :param nci: add a constraining potential for non-covalent interactions (not supported in ETKDG)
82
- :param conf_opt_method: method for the optimization
84
+ :param max_confs: maximum number of conformers to keep
83
85
 
84
86
  New:
85
87
  :param num_initial_confs: number of initial conformers to generate
@@ -87,7 +89,6 @@ class ETKDGSettings(ConformerGenSettings):
87
89
  :param num_confs_taken: number of final conformers to take
88
90
  :param max_mmff_energy: MMFF energy cutoff
89
91
  :param max_mmff_iterations: MMFF optimization iterations
90
- :param max_confs: maximum number of conformers to keep
91
92
  """
92
93
 
93
94
  num_initial_confs: int = 300
@@ -117,10 +118,10 @@ class ETKDGSettings(ConformerGenSettings):
117
118
  case Mode.RECKLESS:
118
119
  self.num_initial_confs = 200
119
120
  self.num_confs_considered = 50
120
- self.max_confs = 20 if self.max_confs is _sentinel else self.max_confs
121
+ self.max_confs = 20 if self.max_confs is None else self.max_confs
121
122
  self.max_mmff_energy = 20
122
123
  case Mode.RAPID:
123
- self.max_confs = 50 if self.max_confs is _sentinel else self.max_confs
124
+ self.max_confs = 50 if self.max_confs is None else self.max_confs
124
125
  self.conf_opt_method = Method.GFN0_XTB
125
126
  case _:
126
127
  raise NotImplementedError(f"Unsupported mode: {self.mode}")
@@ -194,6 +195,7 @@ class iMTDSettings(ConformerGenSettings, ABC):
194
195
  :param screening: post-generation screening settings (not used)
195
196
  :param constraints: constraints to add
196
197
  :param nci: add an ellipsoide potential around the input structure
198
+ :param max_confs: maximum number of conformers to keep
197
199
 
198
200
  New:
199
201
  :param mtd_method: method for the metadynamics
@@ -216,11 +218,11 @@ class iMTDSettings(ConformerGenSettings, ABC):
216
218
  if self.reopt is _sentinel:
217
219
  raise ValueError("Must specify reopt with MANUAL mode")
218
220
  case Mode.RECKLESS: # GFN-FF//MTD(GFN-FF)
219
- self.max_confs = 20 if self.max_confs is _sentinel else self.max_confs
221
+ self.max_confs = 20 if self.max_confs is None else self.max_confs
220
222
  self.speed = iMTDSpeeds.MEGAQUICK
221
223
  self.reopt = check_sentinel(self.reopt, True)
222
224
  case Mode.RAPID: # GFN0//MTD(GFN-FF)
223
- self.max_confs = 50 if self.max_confs is _sentinel else self.max_confs
225
+ self.max_confs = 50 if self.max_confs is None else self.max_confs
224
226
  self.speed = iMTDSpeeds.SUPERQUICK
225
227
  self.conf_opt_method = Method.GFN0_XTB
226
228
  self.reopt = check_sentinel(self.reopt, True)
@@ -258,11 +260,16 @@ class ConformerGenMixin(BaseModel):
258
260
 
259
261
  :param conf_gen_mode: Mode for calculations
260
262
  :param conf_gen_settings: settings for conformer generation
263
+ :param constraints: constraints to add
264
+ :param nci: add a constraining potential for non-covalent interactions
265
+ :param max_confs: maximum number of conformers to keep
261
266
  """
262
267
 
263
268
  conf_gen_mode: Mode = Mode.RAPID
264
269
  conf_gen_settings: ConformerGenSettings = _sentinel # type: ignore [assignment]
265
270
  constraints: Sequence[Constraint] = tuple()
271
+ nci: bool = False
272
+ max_confs: int | None = None
266
273
 
267
274
  @model_validator(mode="after")
268
275
  def validate_and_build_conf_gen_settings(self) -> Self:
@@ -276,10 +283,10 @@ class ConformerGenMixin(BaseModel):
276
283
  raise ValueError("Must specify conf_gen_settings with MANUAL mode")
277
284
 
278
285
  case Mode.RECKLESS | Mode.RAPID:
279
- # ETKDGSettings will error if constraints added
280
- self.conf_gen_settings = ETKDGSettings(mode=self.conf_gen_mode, constraints=self.constraints)
286
+ # ETKDGSettings will error if constraints or nci are set
287
+ self.conf_gen_settings = ETKDGSettings(mode=self.conf_gen_mode, constraints=self.constraints, nci=self.nci, max_confs=self.max_confs)
281
288
  case Mode.CAREFUL | Mode.METICULOUS:
282
- self.conf_gen_settings = iMTDSettings(mode=self.conf_gen_mode, constraints=self.constraints)
289
+ self.conf_gen_settings = iMTDSettings(mode=self.conf_gen_mode, constraints=self.constraints, nci=self.nci, max_confs=self.max_confs)
283
290
 
284
291
  case _:
285
292
  raise NotImplementedError(f"Unsupported mode: {self.conf_gen_mode}")
@@ -291,22 +298,23 @@ class ConformerSearchMixin(ConformerGenMixin, MultiStageOptMixin):
291
298
  """
292
299
  Mixin for workflows that need conformer search—a combination of conformer generation and optimization.
293
300
 
294
- Inherited:
301
+ Inherited (ConformerGenMixin):
295
302
  :param conf_gen_mode: Mode for conformer generation
296
303
  :param mso_mode: Mode for MultiStageOptSettings
297
304
  :param conf_gen_settings: settings for conformer generation
305
+ :param nci: add a constraining potential for non-covalent interactions
306
+
307
+ Inherited (MultiStageOptMixin):
298
308
  :param multistage_opt_settings: set by mso_mode unless mode=MANUAL (ignores additional settings if set)
299
309
  :param solvent: solvent to use
300
310
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
301
- :param constraints: constraints to add (diamond inheritance, works as expected)
302
311
  :param transition_state: whether this is a transition state
312
+ :param frequencies: whether to calculate frequencies
303
313
 
304
- Overridden:
305
- :param frequencies: whether to calculate frequencies (turned off)
314
+ Inherited (Both):
315
+ :param constraints: constraints to add (diamond inheritance, works as expected)
306
316
  """
307
317
 
308
- frequencies: bool = False
309
-
310
318
  def __str__(self) -> str:
311
319
  """Return a string representation of the ConformerSearch workflow."""
312
320
  return repr(self)
@@ -315,6 +323,45 @@ class ConformerSearchMixin(ConformerGenMixin, MultiStageOptMixin):
315
323
  """Return a string representation of the ConformerSearch workflow."""
316
324
  return f"<{type(self).__name__} {self.conf_gen_mode.name} {self.mso_mode.name}>"
317
325
 
326
+ @model_validator(mode="after")
327
+ def deduplicate(self) -> Self:
328
+ """
329
+ Deduplicate optimizations between conf_gen and multistage_opt.
330
+
331
+ Also affects Manual Mode.
332
+ """
333
+ cgs = self.conf_gen_settings
334
+ msos = self.multistage_opt_settings
335
+
336
+ if self.transition_state or not msos.optimization_settings:
337
+ return self
338
+
339
+ first_opt = msos.optimization_settings[0]
340
+ if cgs.conf_opt_method != first_opt.method or "optimize" not in first_opt.tasks:
341
+ return self
342
+
343
+ first_opt.tasks.remove(Task.OPTIMIZE)
344
+ if msos.singlepoint_settings or len(msos.optimization_settings) > 1:
345
+ if (not first_opt.tasks) or first_opt.tasks == ["singlepoint"]:
346
+ msos.optimization_settings = msos.optimization_settings[1:]
347
+
348
+ return self
349
+
350
+ @model_validator(mode="after")
351
+ def remove_ts_constraints(self) -> Self:
352
+ """
353
+ Remove constraints from optimization if a TS.
354
+
355
+ Also affects Manual Mode.
356
+ """
357
+ msos = self.multistage_opt_settings
358
+ if msos.transition_state and msos.constraints:
359
+ msos.constraints = []
360
+ for opt_set in msos.optimization_settings:
361
+ opt_set.opt_settings.constraints = []
362
+
363
+ return self
364
+
318
365
 
319
366
  class ConformerSearchWorkflow(ConformerSearchMixin, Workflow):
320
367
  """
@@ -330,7 +377,7 @@ class ConformerSearchWorkflow(ConformerSearchMixin, Workflow):
330
377
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
331
378
  :param constraints: constraints to add
332
379
  :param transition_state: whether this is a transition state
333
- :param frequencies: whether to calculate frequencies (turned off)
380
+ :param frequencies: whether to calculate frequencies
334
381
 
335
382
  Ignored:
336
383
  :param mode: Mode to use (not used)
@@ -5,7 +5,7 @@ Descriptors = dict[str, dict[str, float] | tuple[float | None, ...] | float]
5
5
 
6
6
 
7
7
  class DescriptorsWorkflow(Workflow):
8
- # uuid of optimization
8
+ # UUID of optimization
9
9
  optimization: UUID | None = None
10
10
 
11
11
  descriptors: Descriptors | None = None
@@ -1,4 +1,6 @@
1
- from pydantic import NonNegativeFloat, NonNegativeInt
1
+ from typing import Annotated, Callable
2
+
3
+ from pydantic import AfterValidator, NonNegativeFloat, NonNegativeInt
2
4
 
3
5
  from ..base import Base
4
6
  from ..settings import Settings
@@ -6,23 +8,34 @@ from ..types import UUID, FloatPerAtom, Matrix3x3, Vector3D
6
8
  from .workflow import Workflow
7
9
 
8
10
 
11
+ def round_float(round_to: int) -> Callable[[float], float]:
12
+ """Return a function that rounds a float to a given number of decimal places."""
13
+
14
+ def inner_round(v: float) -> float:
15
+ return round(v, round_to)
16
+
17
+ return inner_round
18
+
19
+
9
20
  class PropertyCubePoint(Base):
10
- x: float
11
- y: float
12
- z: float
13
- val: float
21
+ """A point in a cube file, all values rounded to 6 decimal places."""
22
+
23
+ x: Annotated[float, AfterValidator(round_float(3))]
24
+ y: Annotated[float, AfterValidator(round_float(3))]
25
+ z: Annotated[float, AfterValidator(round_float(3))]
26
+ val: Annotated[float, AfterValidator(round_float(6))]
14
27
 
15
28
 
16
29
  class PropertyCube(Base):
17
- """
18
- Represents a "cubefile" of some property.
19
- """
30
+ """Represents a "cubefile" of some property."""
20
31
 
21
- cube_data: list[PropertyCubePoint]
32
+ cube_points: list[PropertyCubePoint]
22
33
 
23
34
 
24
35
  class MolecularOrbitalCube(PropertyCube):
25
36
  """
37
+ Cube of a molecular orbital.
38
+
26
39
  Inherits `cube_data`.
27
40
  """
28
41
 
@@ -32,43 +45,48 @@ class MolecularOrbitalCube(PropertyCube):
32
45
 
33
46
  class ElectronicPropertiesWorkflow(Workflow):
34
47
  """
35
- Workflow for computing electronic properties!
48
+ Workflow for computing electronic properties.
36
49
 
37
50
  Inherited
38
- :param initial_molecule: molecule of interest
51
+ :param initial_molecule: Molecule of interest
39
52
 
40
53
  Config settings:
41
- :param settings: the level of theory to use
42
- :param compute_density_cube: whether or not to compute the density on a cube
43
- :param compute_electrostatic_potential_cube: whether or not to compute the electrostatic potential on a cube
44
- :param compute_num_occupied_orbitals: how many occupied orbitals to save
45
- :param compute_num_virtual_orbitals: how many virtual orbitals to save
54
+ :param settings: settings for the calculation
55
+ :param compute_density_cube: whether to compute the density cube
56
+ :param compute_electrostatic_potential_cube: whether to compute the electrostatic potential cube
57
+ :param compute_num_occupied_orbitals: number of occupied orbitals to save
58
+ :param compute_num_virtual_orbitals: number of virtual orbitals to save
46
59
 
47
60
  Populated while running:
48
- :param calculation: the UUID of the calculation
49
- :param dipole: the dipole moment
50
- :param quadrupole: the quadrupole moment
51
- :param mulliken_charges: the Mulliken charges
52
- :param lowdin_charges: the Lowdin charges
53
- :param wiberg_bond_orders: the Wiberg bond orders (`atom1`, `atom2`, `order`)
54
- :param mayer_bond_orders: the Mayer bond orders (`atom1`, `atom2`, `order`)
55
- :param density_cube: the electron density, as a cube
56
- :param electrostatic_potential_cube: the electrostatic potential, as a cube
57
- :param molecular_orbitals_alpha: for open-shell species (UHF/ROHF), a dict containing the alpha MOs
58
- (The key is the absolute orbital index.)
59
- :param molecular_orbitals_beta: for open-shell species (UHF/ROHF), a dict containing the beta MOs
60
- (The key is the absolute orbital index.)
61
- :param molecular_orbitals: for closed-shell species (RHF), a dict containing the MOs
62
- (The key is the absolute orbital index.)
61
+ :param calc_uuid: UUID of the calculation
62
+ :param dipole: dipole moment
63
+ :param quadrupole: quadrupole moment
64
+ :param lowdin_charges: Löwdin charges
65
+ :param mulliken_charges: Mulliken charges
66
+ :param wiberg_bond_orders: Wiberg bond orders (`atom1`, `atom2`, `order`)
67
+ :param mayer_bond_orders: Mayer bond orders (`atom1`, `atom2`, `order`)
68
+
69
+ :param density_cube: electron density, as a cube
70
+ :param density_cube_alpha: α electron density, as a cube
71
+ :param density_cube_beta: β electron density, as a cube
72
+ :param density_cube_difference: difference spin densities, as a cube
73
+
74
+ :param electrostatic_potential_cube: electrostatic potential, as a cube
75
+
76
+ :param molecular_orbitals: MOs, key is absolute orbital index (for closed-shell species (RHF))
77
+ :param molecular_orbitals_alpha: α MOs, key is absolute orbital index (for open-shell species (UHF/ROHF))
78
+ :param molecular_orbitals_beta: β MOs, key is absolute orbital index (for open-shell species (UHF/ROHF))
63
79
  """
64
80
 
81
+ # Config settings
65
82
  settings: Settings
66
83
  compute_density_cube: bool = True
67
84
  compute_electrostatic_potential_cube: bool = True
68
85
  compute_num_occupied_orbitals: NonNegativeInt = 1
69
86
  compute_num_virtual_orbitals: NonNegativeInt = 1
70
87
 
71
- calculation: UUID | None = None
88
+ # Results
89
+ calc_uuid: UUID | None = None
72
90
 
73
91
  dipole: Vector3D | None = None
74
92
  quadrupole: Matrix3x3 | None = None
@@ -80,7 +98,12 @@ class ElectronicPropertiesWorkflow(Workflow):
80
98
  mayer_bond_orders: list[tuple[NonNegativeInt, NonNegativeInt, NonNegativeFloat]] = []
81
99
 
82
100
  density_cube: PropertyCube | None = None
101
+ density_cube_alpha: PropertyCube | None = None
102
+ density_cube_beta: PropertyCube | None = None
103
+ density_cube_difference: PropertyCube | None = None
104
+
83
105
  electrostatic_potential_cube: PropertyCube | None = None
106
+
107
+ molecular_orbitals: dict[NonNegativeInt, MolecularOrbitalCube] = {}
84
108
  molecular_orbitals_alpha: dict[NonNegativeInt, MolecularOrbitalCube] = {}
85
109
  molecular_orbitals_beta: dict[NonNegativeInt, MolecularOrbitalCube] = {}
86
- molecular_orbitals: dict[NonNegativeInt, MolecularOrbitalCube] = {}
@@ -3,7 +3,7 @@ from .workflow import Workflow
3
3
 
4
4
 
5
5
  class FukuiIndexWorkflow(Workflow):
6
- # uuid of optimization
6
+ # UUID of optimization
7
7
  optimization: UUID | None = None
8
8
 
9
9
  global_electrophilicity_index: float | None = None
@@ -0,0 +1,18 @@
1
+ from ..base import Base
2
+ from ..types import UUID
3
+ from .workflow import Workflow
4
+
5
+
6
+ class HydrogenBondAcceptorSite(Base):
7
+ atom_idx: int # zero-indexed
8
+ pkbhx: float
9
+ position: tuple[float, float, float]
10
+ name: str | None = None
11
+
12
+
13
+ class HydrogenBondBasicityWorkflow(Workflow):
14
+ # UUID of optimization
15
+ optimization: UUID | None = None
16
+
17
+ # hydrogen-bond-acceptor sites
18
+ hba_sites: list[HydrogenBondAcceptorSite] = [] # noqa: RUF012
@@ -1,6 +1,6 @@
1
1
  from typing import Self
2
2
 
3
- from pydantic import PositiveFloat, PositiveInt, model_validator
3
+ from pydantic import PositiveFloat, PositiveInt, computed_field, model_validator
4
4
 
5
5
  from ..base import Base, LowercaseStrEnum
6
6
  from ..constraint import PairwiseHarmonicConstraint, SphericalHarmonicConstraint
@@ -12,6 +12,7 @@ from .workflow import Workflow
12
12
  class MolecularDynamicsInitialization(LowercaseStrEnum):
13
13
  RANDOM = "random"
14
14
  QUASICLASSICAL = "quasiclassical"
15
+ READ = "read"
15
16
 
16
17
 
17
18
  class ThermodynamicEnsemble(LowercaseStrEnum):
@@ -28,7 +29,13 @@ class Frame(Base):
28
29
  pressure: float
29
30
  temperature: float
30
31
  volume: float
31
- energy: float
32
+ potential_energy: float # kcal/mol
33
+ kinetic_energy: float # kcal/mol
34
+
35
+ @computed_field # type: ignore[misc, prop-decorator, unused-ignore]
36
+ @property
37
+ def energy(self) -> float:
38
+ return self.potential_energy + self.kinetic_energy
32
39
 
33
40
 
34
41
  class MolecularDynamicsSettings(Base):
@@ -37,6 +44,7 @@ class MolecularDynamicsSettings(Base):
37
44
 
38
45
  timestep: PositiveFloat = 1.0 # fs
39
46
  num_steps: PositiveInt = 500
47
+ save_interval: PositiveInt = 10
40
48
 
41
49
  confining_constraint: SphericalHarmonicConstraint | None = None
42
50
 
@@ -63,7 +71,5 @@ class MolecularDynamicsWorkflow(Workflow):
63
71
  calc_settings: Settings
64
72
  calc_engine: str | None = None
65
73
 
66
- save_interval: PositiveInt = 10
67
-
68
- # uuids of scan points
74
+ # UUIDs of scan points
69
75
  frames: list[Frame] = []
@@ -51,10 +51,10 @@ class MultiStageOptSettings(BaseModel):
51
51
  optimization_settings: Sequence[Settings] = tuple()
52
52
  singlepoint_settings: Settings | None = None
53
53
  solvent: Solvent | None = None
54
- xtb_preopt: bool | None = None
54
+ xtb_preopt: bool = False
55
55
  constraints: Sequence[Constraint] = tuple()
56
56
  transition_state: bool = False
57
- frequencies: bool = True
57
+ frequencies: bool = False
58
58
 
59
59
  def __str__(self) -> str:
60
60
  return repr(self)
@@ -152,7 +152,6 @@ class MultiStageOptSettings(BaseModel):
152
152
  self.singlepoint_settings = sp(Method.GFN2_XTB, solvent=self.solvent)
153
153
 
154
154
  case Mode.RAPID:
155
- self.xtb_preopt = bool(self.xtb_preopt)
156
155
  self.optimization_settings = [
157
156
  *gfn0_pre_opt * self.xtb_preopt,
158
157
  opt(Method.GFN2_XTB, freq=self.frequencies),
@@ -160,7 +159,6 @@ class MultiStageOptSettings(BaseModel):
160
159
  self.singlepoint_settings = sp(Method.R2SCAN3C, solvent=self.solvent)
161
160
 
162
161
  case Mode.CAREFUL:
163
- self.xtb_preopt = (self.xtb_preopt is None) or self.xtb_preopt
164
162
  self.optimization_settings = [
165
163
  *gfn2_pre_opt * self.xtb_preopt,
166
164
  opt(Method.R2SCAN3C, freq=self.frequencies),
@@ -168,7 +166,6 @@ class MultiStageOptSettings(BaseModel):
168
166
  self.singlepoint_settings = sp(Method.WB97X3C, solvent=self.solvent)
169
167
 
170
168
  case Mode.METICULOUS:
171
- self.xtb_preopt = (self.xtb_preopt is None) or self.xtb_preopt
172
169
  self.optimization_settings = [
173
170
  *gfn2_pre_opt * self.xtb_preopt,
174
171
  opt(Method.R2SCAN3C),
@@ -179,8 +176,6 @@ class MultiStageOptSettings(BaseModel):
179
176
  case mode:
180
177
  raise NotImplementedError(f"Cannot assign settings for {mode=}")
181
178
 
182
- assert self.xtb_preopt is not None
183
-
184
179
 
185
180
  class MultiStageOptWorkflow(Workflow, MultiStageOptSettings):
186
181
  """
@@ -232,10 +227,10 @@ class MultiStageOptMixin(BaseModel):
232
227
  # Need to use a sentinel object to make both mypy and pydantic happy
233
228
  multistage_opt_settings: MultiStageOptSettings = _sentinel_msos # type: ignore [assignment]
234
229
  solvent: Solvent | None = None
235
- xtb_preopt: bool | None = None
230
+ xtb_preopt: bool = False
236
231
  constraints: Sequence[Constraint] = tuple()
237
232
  transition_state: bool = False
238
- frequencies: bool = True
233
+ frequencies: bool = False
239
234
 
240
235
  @model_validator(mode="after")
241
236
  def set_mso_settings(self) -> Self:
@@ -277,14 +272,14 @@ def build_mso_settings(
277
272
  use_solvent_for_opt: bool = False,
278
273
  constraints: list[Constraint] | None = None,
279
274
  transition_state: bool = False,
280
- frequencies: bool = True,
275
+ frequencies: bool = False,
281
276
  ) -> MultiStageOptSettings:
282
277
  """
283
278
  Helper function to construct multi-stage opt settings objects manually.
284
279
 
285
280
  There's no xTB pre-optimization here - add that yourself!
286
281
 
287
- :param optimization_settings: list of opt settings to apply successively
282
+ :param optimization_settings: optimization settings to apply successively
288
283
  :param singlepoint_settings: final single point settings
289
284
  :param mode: Mode for settings, defaults to `MANUAL`
290
285
  :param solvent: solvent to use
@@ -292,7 +287,7 @@ def build_mso_settings(
292
287
  :param constraints: constraints for optimization
293
288
  :param transition_state: whether this is a transition state
294
289
  :param frequencies: whether to calculate frequencies
295
- :returns: the final multistage opt settings
290
+ :returns: MultiStageOptSettings
296
291
  """
297
292
  if constraints is None:
298
293
  constraints = []
@@ -336,3 +331,29 @@ def build_mso_settings(
336
331
  transition_state=transition_state,
337
332
  frequencies=frequencies,
338
333
  )
334
+
335
+
336
+ def multi_stage_opt_settings_from_workflow(msow: MultiStageOptWorkflow) -> MultiStageOptSettings:
337
+ """
338
+ Helper function to convert a MultiStageOptWorkflow to MultiStageOptSettings.
339
+
340
+ :param msow: MultiStageOptWorkflow
341
+ :returns: MultiStageOptSettings
342
+
343
+ >>> from stjames.molecule import Atom, Molecule
344
+ >>> He = Molecule(charge=0, multiplicity=1, atoms=[Atom(atomic_number=2, position=[0, 0, 0])])
345
+ >>> msos = multi_stage_opt_settings_from_workflow(
346
+ ... MultiStageOptWorkflow(initial_molecule=He, mode=Mode.RAPID, solvent="water")
347
+ ... )
348
+ >>> print(msos)
349
+ <MultiStageOptSettings RAPID>
350
+ >>> msos.level_of_theory
351
+ 'r2scan_3c/cpcm(water)//gfn2_xtb'
352
+ >>> msos.solvent
353
+ <Solvent.WATER: 'water'>
354
+ >>> msos.xtb_preopt
355
+ False
356
+ """
357
+ data = dict(msow)
358
+ del data["calculations"]
359
+ return MultiStageOptSettings.construct(**data)
@@ -53,7 +53,7 @@ class RedoxPotentialWorkflow(Workflow, MultiStageOptMixin):
53
53
  redox_type: str | None = None
54
54
  redox_potential: float | None = None
55
55
 
56
- # uuids
56
+ # UUIDs
57
57
  neutral_molecule: UUID | None = None
58
58
  anion_molecule: UUID | None = None
59
59
  cation_molecule: UUID | None = None
stjames/workflows/scan.py CHANGED
@@ -23,7 +23,7 @@ class ScanSettings(Base):
23
23
  num: int
24
24
 
25
25
  def vals(self) -> NDArray[np.float64]:
26
- return np.linspace(self.start, self.stop, self.num)
26
+ return np.linspace(self.start, self.stop, self.num) # type: ignore [return-value, unused-ignore]
27
27
 
28
28
  class Config:
29
29
  from_attributes = True
@@ -34,5 +34,5 @@ class ScanWorkflow(Workflow):
34
34
  calc_settings: Settings
35
35
  calc_engine: str
36
36
 
37
- # uuids of scan points
37
+ # UUIDs of scan points
38
38
  scan_points: list[UUID | None] = []
@@ -52,7 +52,7 @@ class SpinStatesWorkflow(Workflow, MultiStageOptMixin):
52
52
  :param mode: Mode for workflow
53
53
  :param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
54
54
  :param solvent: solvent to use for optimization
55
- :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
55
+ :param xtb_preopt: pre-optimize with xtb
56
56
  :param constraints: constraints to add
57
57
  :param transition_state: whether this is a transition state
58
58
  :param frequencies: whether to calculate frequencies
@@ -10,7 +10,7 @@ class Tautomer(Base):
10
10
  weight: Optional[float] = None
11
11
  predicted_relative_energy: Optional[float] = None
12
12
 
13
- # uuids, optionally
13
+ # UUIDs, optionally
14
14
  structures: list[DBCalculation] = []
15
15
 
16
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stjames
3
- Version: 0.0.45
3
+ Version: 0.0.47
4
4
  Summary: standardized JSON atom/molecule encoding scheme
5
5
  Author-email: Corin Wagen <corin@rowansci.com>
6
6
  Project-URL: Homepage, https://github.com/rowansci/stjames
@@ -8,7 +8,7 @@ Project-URL: Bug Tracker, https://github.com/rowansci/stjames/issues
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: pydantic >=2.4
11
+ Requires-Dist: pydantic>=2.4
12
12
  Requires-Dist: numpy
13
13
 
14
14
  # stjames
@@ -1,22 +1,22 @@
1
1
  stjames/__init__.py,sha256=LkWCylP4VeXyQL0iu5w6yeYK1UHjcUH55hwWXTy4mQQ,576
2
2
  stjames/_deprecated_solvent_settings.py,sha256=gj5j9p3zakIwSTK5_ndqBXJx--IzjZNxZ75z-wipLOo,450
3
3
  stjames/atom.py,sha256=w7q-x9xpBw4sJ1WGrWt65WAaStxhz-m7dugXCYEOpq4,2064
4
- stjames/base.py,sha256=9PvUjBeVSkmA3TaruaB0uvjFMbWYTGKXECISNGAj_AU,1201
4
+ stjames/base.py,sha256=hroQjvC8G88UiK520i1TDqnOlgw_ihmzevQGTO5yeUY,1215
5
5
  stjames/basis_set.py,sha256=wI3M2q9uPf9jhKpAi4E2DrsyKzloDGLRjAlk7krdYgc,949
6
6
  stjames/calculation.py,sha256=O2LwwQ_cOLmDOGXTHA9J71YbUZXigUSbvbLA-fSVm3w,915
7
- stjames/constraint.py,sha256=aD4JkNIyya5uh016R68WLYLd0AK6msgki7Q1kAKGzRs,2203
7
+ stjames/constraint.py,sha256=B6oV0rYjmAWr8gpi5f03gRy_uuqjUURVDVwoez5Cfbg,2442
8
8
  stjames/correction.py,sha256=ZVErCcj4TPyZeKrdvXVjHa0tFynsCaoy96QZUVxWFM8,413
9
- stjames/diis_settings.py,sha256=QHc7L-hktkbOWBYr29byTdqL8lWJzKJiY9XW8ha4Qzo,552
9
+ stjames/diis_settings.py,sha256=4m1EQQWBlpHhMnWopix8qOqJv7QCluvdnV9jSKJDFtE,552
10
10
  stjames/grid_settings.py,sha256=WrSNGc-8_f87YBZYt9Hh7RbhM4MweADoVzwBMcSqcsE,640
11
11
  stjames/int_settings.py,sha256=5HXp8opt5ZyY1UpmfaK7NVloWVLM5jkG0elEEqpVLUo,896
12
12
  stjames/message.py,sha256=Rq6QqmHZKecWxYH8fVyXmuoCCPZv8YinvgykSeorXSU,216
13
- stjames/method.py,sha256=a6QQff-0YsutkOTuOcGrdDW76x9ZexiNLdKzzoE1Vcw,1698
13
+ stjames/method.py,sha256=teV0nQZTu8gRSZdFpMhjHtvLPf-XeTIMy330rYWDeZ8,1855
14
14
  stjames/mode.py,sha256=xw46Cc7f3eTS8i35qECi-8DocAlANhayK3w4akD4HBU,496
15
15
  stjames/molecule.py,sha256=v8NikFHfwOahXSo4VKGSqeHKI2HIoRdNjGE0GkZgNS4,10554
16
16
  stjames/opt_settings.py,sha256=gxXGtjy9l-Q5Wen9eO6T6HHRCuS8rfOofdVQIJj0JcI,550
17
17
  stjames/periodic_cell.py,sha256=JDCyynpamggTNi_HnTnnotRbeSMBfYc-srhD-IwUnrg,996
18
18
  stjames/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- stjames/scf_settings.py,sha256=xMMCQ0hVB4nNFSiWesNQZUa_aLsozSZGYWweAPPDGBg,2356
19
+ stjames/scf_settings.py,sha256=WotVgVrayQ_8PUHP39zVtG7iLT9PV41lpzruttFACP8,2356
20
20
  stjames/settings.py,sha256=tfgEYns6WdsheQ6wpR6uyI8O4s2iTqyH7YWtNQ36k74,8666
21
21
  stjames/solvent.py,sha256=u037tmu-9oa21s-WEDZ7VC7nuNVjkqR2ML4JWjWSME4,1158
22
22
  stjames/status.py,sha256=wTKNcNxStoEHrxxgr_zTyN90NITa3rxMQZzOgrCifEw,332
@@ -30,25 +30,26 @@ stjames/data/isotopes.json,sha256=5ba8QnLrHD_Ypv2xekv2cIRwYrX3MQ19-1FOFtt0RuU,83
30
30
  stjames/data/nist_isotopes.json,sha256=d5DNk1dX0iB1waEYIRR6JMHuA7AuYwSBEgBvb4EKyhM,14300
31
31
  stjames/data/read_nist_isotopes.py,sha256=y10FNjW43QpC45qib7VHsIghEwT7GG5rsNwHdc9osRI,3309
32
32
  stjames/data/symbol_element.json,sha256=vl_buFusTqBd-muYQtMLtTDLy2OtBI6KkBeqkaWRQrg,1186
33
- stjames/workflows/__init__.py,sha256=JwcKWrXtrYKJfe6tPbVy6JKSwYxeEaiJWFDL3cVDXf8,363
33
+ stjames/workflows/__init__.py,sha256=XQ89vfQxqz2cWmwcOMp_GKZFQ-RDm8B1s8PqAeQTK3g,1999
34
34
  stjames/workflows/admet.py,sha256=V8noO0Eb7h2bDFSnj6Pxv4ILm0lGxyVRCi13hE0zmEQ,149
35
35
  stjames/workflows/basic_calculation.py,sha256=q48bpab7ZqmRTR4PsGC6bWkuxqkVdJRM8gysevTYXP0,212
36
- stjames/workflows/bde.py,sha256=QAcG-ifw_BSyPspOH4EsLNqc3M3b2Xeu4-I2cj-SqoE,9697
36
+ stjames/workflows/bde.py,sha256=iNrBiAUJA-VaAB-eFddApUO2xIc5PyPYXNtC2stQ_OU,9667
37
37
  stjames/workflows/conformer.py,sha256=YYwL3l7OaVeea4N9-ihghwa_ieKY6hia9LNbiTraMb0,2732
38
- stjames/workflows/conformer_search.py,sha256=PjFvotJ3BEMQqsirRxwH7wOIsnV12VuyXQCIyC2NSKE,12485
39
- stjames/workflows/descriptors.py,sha256=jQ3RuMi7xk799JZ_AL1ARL3yQfWLG03L_VVsK4KIMeY,281
40
- stjames/workflows/electronic_properties.py,sha256=y4tQl1r6K-Wt_sTjrVyQkq0zukeFQ4KlBrSZgIBZp6Y,3100
41
- stjames/workflows/fukui.py,sha256=F5tw5jTqBimo_GiXuThhRpoxauZE5YadZjObLFDCba8,348
42
- stjames/workflows/molecular_dynamics.py,sha256=Y3xUJaCO0A4VHvhEHlqJWu2IrEfruCRTqfnC6SdJu18,2194
43
- stjames/workflows/multistage_opt.py,sha256=gUHtsl3DRvtaZ13_L8CCzAKwridVRnY-0QBNAN0Fq4g,12964
38
+ stjames/workflows/conformer_search.py,sha256=y64WpdVZcrUit9ub-yWwJ0zNK8ofPlcjOG4K6pV6UEM,14241
39
+ stjames/workflows/descriptors.py,sha256=lRRCsGzad3nIg0wI1090ffaXB0FVh0nRRb2lNxCY0kI,281
40
+ stjames/workflows/electronic_properties.py,sha256=0PqS04CfLM4pzx67Ph3JJTrzc1LCwDTT5E8x4_wW-g8,3888
41
+ stjames/workflows/fukui.py,sha256=CsJ3_gvzqEqcxwYN7bnNIa37F3KlLm7obsU77TGmDgo,348
42
+ stjames/workflows/hydrogen_bond_basicity.py,sha256=xn4AP_PbCUluzrj05ufw3LaN-xBMsAmt2mFsVy1bf-8,455
43
+ stjames/workflows/molecular_dynamics.py,sha256=4HmYETU1VT2BA4-PqAayRZLjnj1WuYxd5bqpIyH9g5k,2465
44
+ stjames/workflows/multistage_opt.py,sha256=0ou-UYMGIrewZIg3QZIgwS_eweYdsh2pRplxgRCqLcE,13572
44
45
  stjames/workflows/pka.py,sha256=zpR90Yv2L-D56o2mGArM8027DWpnFFnay31UR9Xh5Nc,774
45
- stjames/workflows/redox_potential.py,sha256=Jteftsi9SLu2Z4Cq5XpKn9kwn0z3Hkbyfx4Y1p8rCsw,3651
46
- stjames/workflows/scan.py,sha256=hL4Hco3Ns0dntjh2G2HhhWmED1mbt0gA_hsglPQ5Vjg,814
47
- stjames/workflows/spin_states.py,sha256=VcCRr7dV-zpazHTkVWb9qds7_4QpTe-Hz_ECdUG9S_Y,4623
48
- stjames/workflows/tautomer.py,sha256=kZSCHo2Q7LzqtQjF_WyyxjECkndG49T9QOM12hsUkx8,421
46
+ stjames/workflows/redox_potential.py,sha256=e7WOyTIC_1NPfh7amvW7YzqQezcswX9YaXT-qPiePAo,3651
47
+ stjames/workflows/scan.py,sha256=cQ29DKlLCH7_6_1TFATnaM3ac9G1asUoRoDDlQYAfG0,860
48
+ stjames/workflows/spin_states.py,sha256=TXHqB7ClTkkCy1Yfcsv99v2woAhT8oG1-uaF20-oMbQ,4592
49
+ stjames/workflows/tautomer.py,sha256=owZOOfGlohxlaOG1pGpx7dVPvas8ZEnl_9Ms5F0Kpms,421
49
50
  stjames/workflows/workflow.py,sha256=tIu5naADYgYS7kdW8quvGEWHWosBcrIdcD7L86v-uMQ,976
50
- stjames-0.0.45.dist-info/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
51
- stjames-0.0.45.dist-info/METADATA,sha256=cXN2IrU7jfMFtL93PeemKYzPCGNGZgV7MlQQSOnJL2A,1628
52
- stjames-0.0.45.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
53
- stjames-0.0.45.dist-info/top_level.txt,sha256=FYCwxl6quhYOAgG-mnPQcCK8vsVM7B8rIUrO-WrQ_PI,8
54
- stjames-0.0.45.dist-info/RECORD,,
51
+ stjames-0.0.47.dist-info/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
52
+ stjames-0.0.47.dist-info/METADATA,sha256=jJX69FHFW3VDQtIwM7lR7tIZgM_CDeIvbC8qeERRhMU,1627
53
+ stjames-0.0.47.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
54
+ stjames-0.0.47.dist-info/top_level.txt,sha256=FYCwxl6quhYOAgG-mnPQcCK8vsVM7B8rIUrO-WrQ_PI,8
55
+ stjames-0.0.47.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5