CodeEntropy 2.0.0__py3-none-any.whl → 2.1.1__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 (39) hide show
  1. CodeEntropy/__init__.py +1 -1
  2. CodeEntropy/config/argparse.py +20 -14
  3. CodeEntropy/config/runtime.py +3 -3
  4. CodeEntropy/core/logging.py +2 -3
  5. CodeEntropy/entropy/configurational.py +5 -185
  6. CodeEntropy/entropy/graph.py +13 -7
  7. CodeEntropy/entropy/nodes/aggregate.py +9 -4
  8. CodeEntropy/entropy/nodes/configurational.py +17 -25
  9. CodeEntropy/entropy/nodes/orientational.py +84 -0
  10. CodeEntropy/entropy/nodes/vibrational.py +31 -14
  11. CodeEntropy/entropy/orientational.py +38 -79
  12. CodeEntropy/entropy/vibrational.py +36 -4
  13. CodeEntropy/entropy/water.py +9 -8
  14. CodeEntropy/entropy/workflow.py +42 -18
  15. CodeEntropy/levels/axes.py +7 -7
  16. CodeEntropy/levels/dihedrals.py +138 -98
  17. CodeEntropy/levels/forces.py +18 -13
  18. CodeEntropy/levels/frame_dag.py +11 -11
  19. CodeEntropy/levels/hierarchy.py +7 -8
  20. CodeEntropy/levels/level_dag.py +22 -18
  21. CodeEntropy/levels/linalg.py +4 -5
  22. CodeEntropy/levels/mda.py +8 -9
  23. CodeEntropy/levels/neighbors.py +251 -0
  24. CodeEntropy/levels/nodes/accumulators.py +12 -11
  25. CodeEntropy/levels/nodes/beads.py +12 -11
  26. CodeEntropy/levels/nodes/conformations.py +28 -13
  27. CodeEntropy/levels/nodes/covariance.py +45 -45
  28. CodeEntropy/levels/nodes/detect_levels.py +5 -5
  29. CodeEntropy/levels/nodes/detect_molecules.py +3 -3
  30. CodeEntropy/levels/nodes/find_neighbors.py +93 -0
  31. CodeEntropy/levels/search.py +241 -0
  32. CodeEntropy/molecules/grouping.py +7 -7
  33. CodeEntropy/results/reporter.py +37 -38
  34. {codeentropy-2.0.0.dist-info → codeentropy-2.1.1.dist-info}/METADATA +7 -4
  35. codeentropy-2.1.1.dist-info/RECORD +48 -0
  36. codeentropy-2.0.0.dist-info/RECORD +0 -44
  37. {codeentropy-2.0.0.dist-info → codeentropy-2.1.1.dist-info}/WHEEL +0 -0
  38. {codeentropy-2.0.0.dist-info → codeentropy-2.1.1.dist-info}/entry_points.txt +0 -0
  39. {codeentropy-2.0.0.dist-info → codeentropy-2.1.1.dist-info}/licenses/LICENSE +0 -0
CodeEntropy/__init__.py CHANGED
@@ -8,4 +8,4 @@ entropy estimates, supporting a wide range of applications in molecular simulati
8
8
  and statistical mechanics.
9
9
  """
10
10
 
11
- __version__ = "2.0.0"
11
+ __version__ = "2.1.1"
@@ -25,7 +25,7 @@ import glob
25
25
  import logging
26
26
  import os
27
27
  from dataclasses import dataclass
28
- from typing import Any, Dict, Optional, Set
28
+ from typing import Any
29
29
 
30
30
  import yaml
31
31
 
@@ -48,11 +48,11 @@ class ArgSpec:
48
48
  help: str
49
49
  default: Any = None
50
50
  type: Any = None
51
- action: Optional[str] = None
52
- nargs: Optional[str] = None
51
+ action: str | None = None
52
+ nargs: str | None = None
53
53
 
54
54
 
55
- ARG_SPECS: Dict[str, ArgSpec] = {
55
+ ARG_SPECS: dict[str, ArgSpec] = {
56
56
  "top_traj_file": ArgSpec(
57
57
  type=str,
58
58
  nargs="+",
@@ -145,6 +145,12 @@ ARG_SPECS: Dict[str, ArgSpec] = {
145
145
  default=True,
146
146
  help="Use bonded axes to rotate forces for UA level vibrational entropies",
147
147
  ),
148
+ "search_type": ArgSpec(
149
+ type=str,
150
+ default="RAD",
151
+ help="Type of neighbor search to use."
152
+ "Default RAD; grid search is also available",
153
+ ),
148
154
  }
149
155
 
150
156
 
@@ -159,7 +165,7 @@ class ConfigResolver:
159
165
  - validating trajectory-related numeric parameters
160
166
  """
161
167
 
162
- def __init__(self, arg_specs: Optional[Dict[str, ArgSpec]] = None) -> None:
168
+ def __init__(self, arg_specs: dict[str, ArgSpec] | None = None) -> None:
163
169
  """Initialize the manager.
164
170
 
165
171
  Args:
@@ -168,7 +174,7 @@ class ConfigResolver:
168
174
  """
169
175
  self._arg_specs = dict(arg_specs or ARG_SPECS)
170
176
 
171
- def load_config(self, directory_path: str) -> Dict[str, Any]:
177
+ def load_config(self, directory_path: str) -> dict[str, Any]:
172
178
  """Load the first YAML config file found in a directory.
173
179
 
174
180
  The current behavior matches your existing workflow:
@@ -188,7 +194,7 @@ class ConfigResolver:
188
194
 
189
195
  config_path = yaml_files[0]
190
196
  try:
191
- with open(config_path, "r", encoding="utf-8") as file:
197
+ with open(config_path, encoding="utf-8") as file:
192
198
  config = yaml.safe_load(file) or {"run1": {}}
193
199
  logger.info("Loaded configuration from: %s", config_path)
194
200
  return config
@@ -253,7 +259,7 @@ class ConfigResolver:
253
259
  )
254
260
  continue
255
261
 
256
- kwargs: Dict[str, Any] = {}
262
+ kwargs: dict[str, Any] = {}
257
263
  if spec.type is not None:
258
264
  kwargs["type"] = spec.type
259
265
  if spec.default is not None:
@@ -266,7 +272,7 @@ class ConfigResolver:
266
272
  return parser
267
273
 
268
274
  def resolve(
269
- self, args: argparse.Namespace, run_config: Optional[Dict[str, Any]]
275
+ self, args: argparse.Namespace, run_config: dict[str, Any] | None
270
276
  ) -> argparse.Namespace:
271
277
  """Merge CLI arguments with YAML configuration and adjust logging level.
272
278
 
@@ -306,8 +312,8 @@ class ConfigResolver:
306
312
 
307
313
  @staticmethod
308
314
  def _detect_cli_overrides(
309
- args_dict: Dict[str, Any], default_dict: Dict[str, Any]
310
- ) -> Set[str]:
315
+ args_dict: dict[str, Any], default_dict: dict[str, Any]
316
+ ) -> set[str]:
311
317
  """Detect which args were explicitly overridden in the CLI.
312
318
 
313
319
  Args:
@@ -322,8 +328,8 @@ class ConfigResolver:
322
328
  def _apply_yaml_defaults(
323
329
  self,
324
330
  args: argparse.Namespace,
325
- run_config: Dict[str, Any],
326
- cli_provided: Set[str],
331
+ run_config: dict[str, Any],
332
+ cli_provided: set[str],
327
333
  ) -> None:
328
334
  """Apply YAML values onto args for keys not provided by CLI.
329
335
 
@@ -336,7 +342,7 @@ class ConfigResolver:
336
342
  if yaml_value is None or key in cli_provided:
337
343
  continue
338
344
  if key in self._arg_specs:
339
- logger.debug("Using YAML value for %s: %s", key, yaml_value)
345
+ logger.debug(f"Using YAML value for {key}: {yaml_value}")
340
346
  setattr(args, key, yaml_value)
341
347
 
342
348
  def _ensure_defaults(self, args: argparse.Namespace) -> None:
@@ -18,7 +18,7 @@ from __future__ import annotations
18
18
  import logging
19
19
  import os
20
20
  import pickle
21
- from typing import Any, Dict, Optional
21
+ from typing import Any
22
22
 
23
23
  import MDAnalysis as mda
24
24
  import requests
@@ -112,7 +112,7 @@ class CodeEntropyRunner:
112
112
 
113
113
  return new_folder_path
114
114
 
115
- def load_citation_data(self) -> Optional[Dict[str, Any]]:
115
+ def load_citation_data(self) -> dict[str, Any] | None:
116
116
  """Load CITATION.cff from GitHub.
117
117
 
118
118
  If the request fails (offline, blocked, etc.), returns None.
@@ -335,7 +335,7 @@ class CodeEntropyRunner:
335
335
  kcal_units = args.kcal_force_units
336
336
 
337
337
  if forcefile is None:
338
- logger.debug("Loading Universe with %s and %s", tprfile, trrfile)
338
+ logger.debug(f"Loading Universe with {tprfile} and {trrfile}")
339
339
  return mda.Universe(tprfile, trrfile, format=fileformat)
340
340
 
341
341
  return universe_operations.merge_forces(
@@ -16,7 +16,6 @@ from __future__ import annotations
16
16
 
17
17
  import logging
18
18
  import os
19
- from typing import Dict, Optional
20
19
 
21
20
  from rich.console import Console
22
21
  from rich.logging import RichHandler
@@ -55,7 +54,7 @@ class LoggingConfig:
55
54
  handlers: Mapping of handler name to handler instance.
56
55
  """
57
56
 
58
- _console: Optional[Console] = None
57
+ _console: Console | None = None
59
58
 
60
59
  @classmethod
61
60
  def get_console(cls) -> Console:
@@ -80,7 +79,7 @@ class LoggingConfig:
80
79
 
81
80
  self.level = level
82
81
  self.console = self.get_console()
83
- self.handlers: Dict[str, logging.Handler] = {}
82
+ self.handlers: dict[str, logging.Handler] = {}
84
83
 
85
84
  self._setup_handlers()
86
85
 
@@ -10,46 +10,15 @@ This module provides:
10
10
  from __future__ import annotations
11
11
 
12
12
  import logging
13
- from dataclasses import dataclass
14
- from typing import Any, Optional
13
+ from typing import Any
15
14
 
16
15
  import numpy as np
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
20
19
 
21
- @dataclass(frozen=True)
22
- class ConformationConfig:
23
- """Configuration for assigning conformational states from a dihedral.
24
-
25
- Attributes:
26
- bin_width: Histogram bin width in degrees for peak detection.
27
- start: Inclusive start frame index for trajectory slicing.
28
- end: Exclusive end frame index for trajectory slicing.
29
- step: Stride for trajectory slicing (must be positive).
30
- """
31
-
32
- bin_width: int
33
- start: int
34
- end: int
35
- step: int
36
-
37
-
38
20
  class ConformationalEntropy:
39
- """Assign dihedral conformational states and compute conformational entropy.
40
-
41
- This class contains two independent responsibilities:
42
- 1) `assign_conformation`: Map a single dihedral angle time series to discrete
43
- state labels by detecting histogram peaks and assigning the nearest peak.
44
- 2) `conformational_entropy_calculation`: Compute Shannon entropy of the
45
- state distribution (in J/mol/K).
46
-
47
- Notes:
48
- `number_frames` is accepted by `conformational_entropy_calculation` for
49
- compatibility with calling sites that track frame counts, but the entropy
50
- is computed from the observed state counts (i.e., `len(states)`), which is
51
- the correct normalization for the sampled distribution.
52
- """
21
+ """Compute conformational entropy from states information."""
53
22
 
54
23
  _GAS_CONST: float = 8.3144598484848
55
24
 
@@ -61,69 +30,7 @@ class ConformationalEntropy:
61
30
  """
62
31
  pass
63
32
 
64
- def assign_conformation(
65
- self,
66
- data_container: Any,
67
- dihedral: Any,
68
- number_frames: int,
69
- bin_width: int,
70
- start: int,
71
- end: int,
72
- step: int,
73
- ) -> np.ndarray:
74
- """Assign discrete conformational states for a single dihedral.
75
-
76
- The dihedral angle time series is:
77
- 1) Collected across the trajectory slice [start:end:step].
78
- 2) Converted to [0, 360) degrees.
79
- 3) Histogrammed using `bin_width`.
80
- 4) Peaks are identified as bins with locally maximal population.
81
- 5) Each frame is assigned the index of the nearest peak.
82
-
83
- Args:
84
- data_container: MDAnalysis Universe/AtomGroup with a trajectory.
85
- dihedral: Object providing `value()` for the current frame dihedral.
86
- number_frames: Provided for call-site compatibility; not used for sizing.
87
- bin_width: Histogram bin width in degrees.
88
- start: Inclusive start frame index.
89
- end: Exclusive end frame index.
90
- step: Stride for trajectory slicing.
91
-
92
- Returns:
93
- Array of integer state labels of length equal to the trajectory slice.
94
- Returns an empty array if the slice is empty.
95
-
96
- Raises:
97
- ValueError: If `bin_width` or `step` are invalid.
98
- """
99
- _ = number_frames
100
-
101
- config = ConformationConfig(
102
- bin_width=int(bin_width),
103
- start=int(start),
104
- end=int(end),
105
- step=int(step),
106
- )
107
- self._validate_assignment_config(config)
108
-
109
- traj_slice = data_container.trajectory[config.start : config.end : config.step]
110
- n_slice = len(traj_slice)
111
- if n_slice <= 0:
112
- return np.array([], dtype=int)
113
-
114
- phi = self._collect_dihedral_angles(traj_slice, dihedral)
115
- peak_values = self._find_histogram_peaks(phi, config.bin_width)
116
-
117
- if peak_values.size == 0:
118
- return np.zeros(n_slice, dtype=int)
119
-
120
- states = self._assign_nearest_peaks(phi, peak_values)
121
- logger.debug("Final conformations: %s", states)
122
- return states
123
-
124
- def conformational_entropy_calculation(
125
- self, states: Any, number_frames: int
126
- ) -> float:
33
+ def conformational_entropy_calculation(self, states: Any) -> float:
127
34
  """Compute conformational entropy for a sequence of state labels.
128
35
 
129
36
  Entropy is computed as:
@@ -139,8 +46,6 @@ class ConformationalEntropy:
139
46
  Returns:
140
47
  float: Conformational entropy in J/mol/K.
141
48
  """
142
- _ = number_frames
143
-
144
49
  arr = self._to_1d_array(states)
145
50
  if arr is None or arr.size == 0:
146
51
  return 0.0
@@ -154,96 +59,11 @@ class ConformationalEntropy:
154
59
  probs = probs[probs > 0.0]
155
60
 
156
61
  s_conf = -self._GAS_CONST * float(np.sum(probs * np.log(probs)))
157
- logger.debug("Total conformational entropy: %s", s_conf)
62
+ logger.debug(f"Total conformational entropy: {s_conf}")
158
63
  return s_conf
159
64
 
160
65
  @staticmethod
161
- def _validate_assignment_config(config: ConformationConfig) -> None:
162
- """Validate conformation assignment configuration.
163
-
164
- Args:
165
- config: Assignment configuration.
166
-
167
- Raises:
168
- ValueError: If configuration values are invalid.
169
- """
170
- if config.step <= 0:
171
- raise ValueError("step must be a positive integer")
172
- if config.bin_width <= 0 or config.bin_width > 360:
173
- raise ValueError("bin_width must be in the range (0, 360]")
174
- if 360 % config.bin_width != 0:
175
- logger.warning(
176
- "bin_width=%s does not evenly divide 360; histogram bins will be "
177
- "uneven.",
178
- config.bin_width,
179
- )
180
-
181
- @staticmethod
182
- def _collect_dihedral_angles(traj_slice: Any, dihedral: Any) -> np.ndarray:
183
- """Collect dihedral angles for each frame in the trajectory slice.
184
-
185
- Args:
186
- traj_slice: Slice of a trajectory iterable where iterating advances frames.
187
- dihedral: Object with `value()` returning the dihedral in degrees.
188
-
189
- Returns:
190
- Array of dihedral values mapped into [0, 360).
191
- """
192
- phi = np.zeros(len(traj_slice), dtype=float)
193
- for i, _ts in enumerate(traj_slice):
194
- value = float(dihedral.value())
195
- if value < 0.0:
196
- value += 360.0
197
- phi[i] = value
198
- return phi
199
-
200
- @staticmethod
201
- def _find_histogram_peaks(phi: np.ndarray, bin_width: int) -> np.ndarray:
202
- """Identify peak bin centers from a histogram of dihedral angles.
203
-
204
- A peak is defined as a bin whose population is greater than or equal to
205
- its immediate neighbors (with circular handling at the final bin).
206
-
207
- Args:
208
- phi: Dihedral angles in degrees, in [0, 360).
209
- bin_width: Histogram bin width in degrees.
210
-
211
- Returns:
212
- 1D array of peak bin center values (degrees). Empty if no peaks found.
213
- """
214
- number_bins = int(360 / bin_width)
215
- popul, bin_edges = np.histogram(phi, bins=number_bins, range=(0.0, 360.0))
216
- bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
217
-
218
- peaks: list[float] = []
219
- for idx in range(number_bins):
220
- if popul[idx] == 0:
221
- continue
222
-
223
- left = popul[idx - 1] if idx > 0 else popul[number_bins - 1]
224
- right = popul[idx + 1] if idx < number_bins - 1 else popul[0]
225
-
226
- if popul[idx] >= left and popul[idx] >= right:
227
- peaks.append(float(bin_centers[idx]))
228
-
229
- return np.asarray(peaks, dtype=float)
230
-
231
- @staticmethod
232
- def _assign_nearest_peaks(phi: np.ndarray, peak_values: np.ndarray) -> np.ndarray:
233
- """Assign each phi value to the index of its nearest peak.
234
-
235
- Args:
236
- phi: Dihedral angles in degrees.
237
- peak_values: Peak centers (degrees).
238
-
239
- Returns:
240
- Integer state labels aligned with `phi`.
241
- """
242
- distances = np.abs(phi[:, None] - peak_values[None, :])
243
- return np.argmin(distances, axis=1).astype(int)
244
-
245
- @staticmethod
246
- def _to_1d_array(states: Any) -> Optional[np.ndarray]:
66
+ def _to_1d_array(states: Any) -> np.ndarray | None:
247
67
  """Convert a state sequence into a 1D numpy array.
248
68
 
249
69
  Args:
@@ -15,18 +15,19 @@ from __future__ import annotations
15
15
 
16
16
  import logging
17
17
  from dataclasses import dataclass
18
- from typing import Any, Dict
18
+ from typing import Any
19
19
 
20
20
  import networkx as nx
21
21
 
22
22
  from CodeEntropy.entropy.nodes.aggregate import AggregateEntropyNode
23
23
  from CodeEntropy.entropy.nodes.configurational import ConfigurationalEntropyNode
24
+ from CodeEntropy.entropy.nodes.orientational import OrientationalEntropyNode
24
25
  from CodeEntropy.entropy.nodes.vibrational import VibrationalEntropyNode
25
26
 
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
 
29
- SharedData = Dict[str, Any]
30
+ SharedData = dict[str, Any]
30
31
 
31
32
 
32
33
  @dataclass(frozen=True)
@@ -57,9 +58,9 @@ class EntropyGraph:
57
58
  def __init__(self) -> None:
58
59
  """Initialize an empty entropy graph."""
59
60
  self._graph: nx.DiGraph = nx.DiGraph()
60
- self._nodes: Dict[str, Any] = {}
61
+ self._nodes: dict[str, Any] = {}
61
62
 
62
- def build(self) -> "EntropyGraph":
63
+ def build(self) -> EntropyGraph:
63
64
  """Populate the graph with the standard entropy workflow.
64
65
 
65
66
  Returns:
@@ -68,10 +69,15 @@ class EntropyGraph:
68
69
  specs = (
69
70
  NodeSpec("vibrational_entropy", VibrationalEntropyNode()),
70
71
  NodeSpec("configurational_entropy", ConfigurationalEntropyNode()),
72
+ NodeSpec("orientational_entropy", OrientationalEntropyNode()),
71
73
  NodeSpec(
72
74
  "aggregate_entropy",
73
75
  AggregateEntropyNode(),
74
- deps=("vibrational_entropy", "configurational_entropy"),
76
+ deps=(
77
+ "vibrational_entropy",
78
+ "configurational_entropy",
79
+ "orientational_entropy",
80
+ ),
75
81
  ),
76
82
  )
77
83
 
@@ -82,7 +88,7 @@ class EntropyGraph:
82
88
 
83
89
  def execute(
84
90
  self, shared_data: SharedData, *, progress: object | None = None
85
- ) -> Dict[str, Any]:
91
+ ) -> dict[str, Any]:
86
92
  """Execute the entropy graph in topological order.
87
93
 
88
94
  Nodes are executed in dependency order (topological sort). Each node reads
@@ -105,7 +111,7 @@ class EntropyGraph:
105
111
  Raises:
106
112
  KeyError: If a node name is missing from the internal node registry.
107
113
  """
108
- results: Dict[str, Any] = {}
114
+ results: dict[str, Any] = {}
109
115
 
110
116
  for node_name in nx.topological_sort(self._graph):
111
117
  node = self._nodes[node_name]
@@ -2,10 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from collections.abc import Mapping, MutableMapping
5
6
  from dataclasses import dataclass
6
- from typing import Any, Dict, Mapping, MutableMapping, Optional
7
+ from typing import Any
7
8
 
8
- EntropyResults = Dict[str, Any]
9
+ EntropyResults = dict[str, Any]
9
10
 
10
11
 
11
12
  @dataclass(frozen=True, slots=True)
@@ -25,11 +26,12 @@ class AggregateEntropyNode:
25
26
 
26
27
  vibrational_key: str = "vibrational_entropy"
27
28
  configurational_key: str = "configurational_entropy"
29
+ orientational_key: str = "orientational_entropy"
28
30
  output_key: str = "entropy_results"
29
31
 
30
32
  def run(
31
33
  self, shared_data: MutableMapping[str, Any], **_: Any
32
- ) -> Dict[str, EntropyResults]:
34
+ ) -> dict[str, EntropyResults]:
33
35
  """Run the aggregation step.
34
36
 
35
37
  Args:
@@ -69,10 +71,13 @@ class AggregateEntropyNode:
69
71
  "configurational_entropy": self._get_optional(
70
72
  shared_data, self.configurational_key
71
73
  ),
74
+ "orientational_entropy": self._get_optional(
75
+ shared_data, self.orientational_key
76
+ ),
72
77
  }
73
78
 
74
79
  @staticmethod
75
- def _get_optional(shared_data: Mapping[str, Any], key: str) -> Optional[Any]:
80
+ def _get_optional(shared_data: Mapping[str, Any], key: str) -> Any | None:
76
81
  """Fetch an optional value from shared data.
77
82
 
78
83
  Args:
@@ -3,16 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from collections.abc import Iterable, Mapping, MutableMapping, Sequence
6
7
  from typing import (
7
8
  Any,
8
- Dict,
9
- Iterable,
10
- Mapping,
11
- MutableMapping,
12
- Optional,
13
- Sequence,
14
- Tuple,
15
- Union,
16
9
  )
17
10
 
18
11
  import numpy as np
@@ -23,8 +16,8 @@ logger = logging.getLogger(__name__)
23
16
 
24
17
  GroupId = int
25
18
  ResidueId = int
26
- StateKey = Tuple[GroupId, ResidueId]
27
- StateSequence = Union[Sequence[Any], np.ndarray]
19
+ StateKey = tuple[GroupId, ResidueId]
20
+ StateSequence = Sequence[Any] | np.ndarray
28
21
 
29
22
 
30
23
  class ConfigurationalEntropyNode:
@@ -36,7 +29,7 @@ class ConfigurationalEntropyNode:
36
29
  Results are written back into ``shared_data["configurational_entropy"]``.
37
30
  """
38
31
 
39
- def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> Dict[str, Any]:
32
+ def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> dict[str, Any]:
40
33
  """Execute configurational entropy calculation.
41
34
 
42
35
  Args:
@@ -58,7 +51,7 @@ class ConfigurationalEntropyNode:
58
51
  ce = self._build_entropy_engine()
59
52
 
60
53
  fragments = universe.atoms.fragments
61
- results: Dict[int, Dict[str, float]] = {}
54
+ results: dict[int, dict[str, float]] = {}
62
55
 
63
56
  for group_id, mol_ids in groups.items():
64
57
  results[group_id] = {"ua": 0.0, "res": 0.0, "poly": 0.0}
@@ -104,9 +97,9 @@ class ConfigurationalEntropyNode:
104
97
 
105
98
  def _get_state_containers(
106
99
  self, shared_data: Mapping[str, Any]
107
- ) -> Tuple[
108
- Dict[StateKey, StateSequence],
109
- Union[Dict[GroupId, StateSequence], Sequence[Optional[StateSequence]]],
100
+ ) -> tuple[
101
+ dict[StateKey, StateSequence],
102
+ dict[GroupId, StateSequence] | Sequence[StateSequence | None],
110
103
  ]:
111
104
  """Retrieve conformational state containers.
112
105
 
@@ -144,7 +137,7 @@ class ConfigurationalEntropyNode:
144
137
  residues: Iterable[Any],
145
138
  states_ua: Mapping[StateKey, StateSequence],
146
139
  n_frames: int,
147
- reporter: Optional[Any],
140
+ reporter: Any | None,
148
141
  ) -> float:
149
142
  """Compute united atom entropy for a group.
150
143
 
@@ -163,7 +156,7 @@ class ConfigurationalEntropyNode:
163
156
 
164
157
  for res_id, res in enumerate(residues):
165
158
  states = states_ua.get((group_id, res_id))
166
- val = self._entropy_or_zero(ce, states, n_frames)
159
+ val = self._entropy_or_zero(ce, states)
167
160
  total += val
168
161
 
169
162
  if reporter is not None:
@@ -186,29 +179,28 @@ class ConfigurationalEntropyNode:
186
179
  *,
187
180
  ce: ConformationalEntropy,
188
181
  group_id: int,
189
- states_res: Union[Dict[int, StateSequence], Sequence[Optional[StateSequence]]],
182
+ states_res: dict[int, StateSequence] | Sequence[StateSequence | None],
190
183
  n_frames: int,
191
184
  ) -> float:
192
185
  """Compute residue-level entropy for a group."""
193
186
  group_states = self._get_group_states(states_res, group_id)
194
- return self._entropy_or_zero(ce, group_states, n_frames)
187
+ return self._entropy_or_zero(ce, group_states)
195
188
 
196
189
  def _entropy_or_zero(
197
190
  self,
198
191
  ce: ConformationalEntropy,
199
- states: Optional[StateSequence],
200
- n_frames: int,
192
+ states: StateSequence | None,
201
193
  ) -> float:
202
194
  """Return entropy value or zero if no state data exists."""
203
195
  if not self._has_state_data(states):
204
196
  return 0.0
205
- return float(ce.conformational_entropy_calculation(states, n_frames))
197
+ return float(ce.conformational_entropy_calculation(states))
206
198
 
207
199
  @staticmethod
208
200
  def _get_group_states(
209
- states_res: Union[Dict[int, StateSequence], Sequence[Optional[StateSequence]]],
201
+ states_res: dict[int, StateSequence] | Sequence[StateSequence | None],
210
202
  group_id: int,
211
- ) -> Optional[StateSequence]:
203
+ ) -> StateSequence | None:
212
204
  """Fetch group states from container."""
213
205
  if isinstance(states_res, dict):
214
206
  return states_res.get(group_id)
@@ -217,7 +209,7 @@ class ConfigurationalEntropyNode:
217
209
  return None
218
210
 
219
211
  @staticmethod
220
- def _has_state_data(states: Optional[StateSequence]) -> bool:
212
+ def _has_state_data(states: StateSequence | None) -> bool:
221
213
  """Check if state container has usable data."""
222
214
  if states is None:
223
215
  return False