PackLab 0.8.0__cp314-cp314-win_amd64.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 (43) hide show
  1. PackLab/__init__.py +13 -0
  2. PackLab/_unit_registry.cp314-win_amd64.pyd +0 -0
  3. PackLab/_version.py +24 -0
  4. PackLab/analytical/__init__.py +3 -0
  5. PackLab/analytical/domain.cp314-win_amd64.pyd +0 -0
  6. PackLab/analytical/domain.pyi +19 -0
  7. PackLab/analytical/grid.py +55 -0
  8. PackLab/analytical/solver.cp314-win_amd64.pyd +0 -0
  9. PackLab/analytical/solver.pyi +21 -0
  10. PackLab/directories.py +14 -0
  11. PackLab/monte_carlo/__init__.py +14 -0
  12. PackLab/monte_carlo/diagnostics.py +198 -0
  13. PackLab/monte_carlo/diagnostics.pyi +30 -0
  14. PackLab/monte_carlo/domain.cp314-win_amd64.pyd +0 -0
  15. PackLab/monte_carlo/domain.pyi +15 -0
  16. PackLab/monte_carlo/estimator.cp314-win_amd64.pyd +0 -0
  17. PackLab/monte_carlo/estimator.pyi +27 -0
  18. PackLab/monte_carlo/metropolis.cp314-win_amd64.pyd +0 -0
  19. PackLab/monte_carlo/metropolis.pyi +36 -0
  20. PackLab/monte_carlo/persistence.py +297 -0
  21. PackLab/monte_carlo/persistence.pyi +27 -0
  22. PackLab/monte_carlo/result.cp314-win_amd64.pyd +0 -0
  23. PackLab/monte_carlo/result.pyi +17 -0
  24. PackLab/monte_carlo/results.py +471 -0
  25. PackLab/monte_carlo/results.pyi +19 -0
  26. PackLab/monte_carlo/simulator.cp314-win_amd64.pyd +0 -0
  27. PackLab/monte_carlo/simulator.pyi +43 -0
  28. PackLab/monte_carlo/statistics.cp314-win_amd64.pyd +0 -0
  29. PackLab/monte_carlo/statistics.pyi +15 -0
  30. PackLab/monte_carlo/structure.py +229 -0
  31. PackLab/monte_carlo/structure.pyi +25 -0
  32. PackLab/py.typed +1 -0
  33. PackLab/samplers.cp314-win_amd64.pyd +0 -0
  34. PackLab/samplers.pyi +31 -0
  35. PackLab/scattering/__init__.py +10 -0
  36. PackLab/scattering/data.py +504 -0
  37. PackLab/scattering/model.py +119 -0
  38. PackLab/scattering/plottings.py +205 -0
  39. PackLab/units.py +5 -0
  40. packlab-0.8.0.dist-info/METADATA +321 -0
  41. packlab-0.8.0.dist-info/RECORD +43 -0
  42. packlab-0.8.0.dist-info/WHEEL +5 -0
  43. packlab-0.8.0.dist-info/licenses/LICENSE +21 -0
PackLab/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from PackLab.units import ureg # noqa: F401
2
+ from PackLab import samplers, monte_carlo, analytical # noqa: F401
3
+
4
+ debug_mode = False # noqa: F401
5
+
6
+
7
+ try:
8
+ from ._version import version as __version__ # noqa: F401
9
+
10
+ except ImportError:
11
+ __version__ = "0.0.0"
12
+
13
+ # -
PackLab/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.8.0'
22
+ __version_tuple__ = version_tuple = (0, 8, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,3 @@
1
+ from PackLab.analytical.domain import PercusYevickDomain # noqa: F401
2
+ from PackLab.analytical.solver import PercusYevickResult, PercusYevickSolver # noqa: F401
3
+ from PackLab.analytical.grid import make_wavenumber_grid # noqa: F401
@@ -0,0 +1,19 @@
1
+ from typing import Any
2
+
3
+ class PercusYevickDomain:
4
+ def __init__(
5
+ self,
6
+ size: Any,
7
+ radii: Any,
8
+ volume_fraction: float,
9
+ number_fractions: Any,
10
+ rounding_mode: Any = ...,
11
+ ) -> None: ...
12
+ size: Any
13
+ radii: Any
14
+ volume_fraction: float
15
+ number_fractions: Any
16
+ particle_densities_per_radius: Any
17
+ def sample_radii(self, number_of_samples: int, seed: int | None = None) -> Any: ...
18
+ def print_bins(self, precision: int = 6) -> None: ...
19
+ def bins_table(self, precision: int = 6) -> str: ...
@@ -0,0 +1,55 @@
1
+ """Utilities for constructing reciprocal-space grids."""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+ from PackLab.units import ureg
8
+
9
+
10
+ def make_wavenumber_grid(
11
+ radial_resolution: Any,
12
+ maximum_distance: Any,
13
+ samples_per_oscillation: int = 12,
14
+ ) -> Any:
15
+ """Create a reciprocal-space grid for the radial inverse transform.
16
+
17
+ Parameters
18
+ ----------
19
+ radial_resolution : pint.Quantity
20
+ Smallest real-space feature to resolve. It determines the maximum
21
+ wavenumber through ``pi / radial_resolution``.
22
+ maximum_distance : pint.Quantity
23
+ Largest real-space distance at which the correlation function will be
24
+ evaluated. It determines the wavenumber spacing.
25
+ samples_per_oscillation : int, default=12
26
+ Samples used for one period of the sinc kernel at ``maximum_distance``.
27
+ Values below 8 are not recommended.
28
+
29
+ Returns
30
+ -------
31
+ pint.Quantity
32
+ A uniformly spaced, zero-inclusive wavenumber grid with units of
33
+ inverse length.
34
+ """
35
+ if samples_per_oscillation < 2:
36
+ raise ValueError("samples_per_oscillation must be at least 2.")
37
+
38
+ resolution_m = radial_resolution.to("meter")
39
+ maximum_distance_m = maximum_distance.to("meter")
40
+ if resolution_m.magnitude <= 0:
41
+ raise ValueError("radial_resolution must be positive.")
42
+ if maximum_distance_m.magnitude <= 0:
43
+ raise ValueError("maximum_distance must be positive.")
44
+
45
+ maximum_wavenumber = np.pi / resolution_m
46
+ wavenumber_step = 2 * np.pi / (samples_per_oscillation * maximum_distance_m)
47
+ number_of_points = int(
48
+ np.ceil((maximum_wavenumber / wavenumber_step).to_base_units().magnitude)
49
+ ) + 1
50
+
51
+ return np.linspace(
52
+ 0.0,
53
+ maximum_wavenumber.to("1 / meter").magnitude,
54
+ number_of_points,
55
+ ) / ureg.meter
@@ -0,0 +1,21 @@
1
+ from typing import Any, Literal
2
+
3
+ class PercusYevickResult:
4
+ radii: Any
5
+ densities: Any
6
+ wavenumber: Any
7
+ distances: Any
8
+ g: Any
9
+ h: Any
10
+ H: Any
11
+
12
+ class PercusYevickSolver:
13
+ def __init__(
14
+ self,
15
+ densities: Any,
16
+ radii: Any,
17
+ wavenumber: Any | Literal["auto"] = "auto",
18
+ radial_resolution: Any | None = None,
19
+ samples_per_oscillation: int = 12,
20
+ ) -> None: ...
21
+ def compute(self, distances: Any) -> PercusYevickResult: ...
PackLab/directories.py ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8; py-indent-offset:4 -*-
3
+
4
+ from pathlib import Path
5
+ import PackLab
6
+
7
+ root_path = Path(PackLab.__path__[0])
8
+
9
+ repository = project_path = Path(root_path)
10
+
11
+ doc_css_path = project_path.parent / "docs/source/_static/default.css"
12
+
13
+ data = repository / "data"
14
+
@@ -0,0 +1,14 @@
1
+ from PackLab.monte_carlo import result as _result # noqa: F401
2
+ from PackLab.monte_carlo.domain import PackingDomain # noqa: F401
3
+ from PackLab.monte_carlo.simulator import RSAOptions, RSASimulator, PackingConfiguration # noqa: F401
4
+ from PackLab.monte_carlo.metropolis import (
5
+ MetropolisOptions as MetropolisOptions,
6
+ MetropolisSimulator as MetropolisSimulator,
7
+ MetropolisStatistics as MetropolisStatistics,
8
+ )
9
+ from PackLab.monte_carlo.statistics import PackingStatistics # noqa: F401
10
+ from PackLab.monte_carlo.estimator import PackingEstimator, PackingEstimate # noqa: F401
11
+ from PackLab.monte_carlo.results import PackingResult # noqa: F401
12
+ from PackLab.monte_carlo.persistence import LoadedPackingConfiguration, load_packing, save_packing # noqa: F401
13
+ from PackLab.monte_carlo.diagnostics import MetropolisDiagnosticReport, run_metropolis_diagnostics # noqa: F401
14
+ from PackLab.monte_carlo.structure import EmpiricalStructure, empirical_structure # noqa: F401
@@ -0,0 +1,198 @@
1
+ """Low-memory diagnostics for fixed-volume Metropolis hard-sphere sampling."""
2
+
3
+
4
+ from dataclasses import dataclass
5
+ from statistics import NormalDist
6
+ from typing import Any, Callable
7
+
8
+ import numpy as np
9
+
10
+ from PackLab.units import ureg
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class MetropolisDiagnosticReport:
15
+ """
16
+ Retained diagnostics from a sampled Metropolis trajectory.
17
+
18
+ Notes
19
+ -----
20
+ These summaries quantify the recorded observable only. They do not prove
21
+ or automatically assert equilibration.
22
+ """
23
+
24
+ sample_sweeps: np.ndarray
25
+ acceptance_rates: np.ndarray
26
+ accepted_move_deltas: np.ndarray
27
+ rejected_move_deltas: np.ndarray
28
+ mean_squared_displacements: Any
29
+ observable: np.ndarray
30
+ observable_name: str
31
+ burn_in_sweeps: int
32
+ autocorrelation_time: float
33
+ effective_sample_size: float
34
+ block_means: np.ndarray
35
+ standard_error: float
36
+ confidence_interval: tuple[float, float]
37
+ confidence_level: float
38
+ final_result: Any
39
+
40
+
41
+ def _validate_integer(name: str, value: int, minimum: int) -> int:
42
+ if isinstance(value, bool) or not isinstance(value, (int, np.integer)) or value < minimum:
43
+ raise ValueError(f"{name} must be an integer greater than or equal to {minimum}.")
44
+ return int(value)
45
+
46
+
47
+ def _autocorrelation_time(values: np.ndarray) -> float:
48
+ centered = values - values.mean()
49
+ variance = np.dot(centered, centered) / values.size
50
+ if variance == 0.0:
51
+ return 0.5
52
+ correlations = np.correlate(centered, centered, mode="full")[values.size - 1 :] / (
53
+ variance * np.arange(values.size, 0, -1)
54
+ )
55
+ tau = 0.5
56
+ for correlation in correlations[1:]:
57
+ if correlation <= 0.0:
58
+ break
59
+ tau += float(correlation)
60
+ return tau
61
+
62
+
63
+ def run_metropolis_diagnostics(
64
+ simulator: Any,
65
+ number_of_sweeps: int,
66
+ *,
67
+ sample_interval: int = 10,
68
+ burn_in_sweeps: int = 0,
69
+ observable: Callable[[Any], float] | None = None,
70
+ observable_name: str = "mean_squared_displacement_m2",
71
+ block_size: int | None = None,
72
+ confidence_level: float = 0.95,
73
+ ) -> MetropolisDiagnosticReport:
74
+ """
75
+ Run sampled chunks of a Metropolis simulation without retaining positions.
76
+
77
+ Parameters
78
+ ----------
79
+ simulator : MetropolisSimulator
80
+ Native public simulator to advance. It is not reset by this function.
81
+ number_of_sweeps : int
82
+ Number of additional sweeps to execute.
83
+ sample_interval : int, default=10
84
+ Number of sweeps between retained observations.
85
+ burn_in_sweeps : int, default=0
86
+ Initial sweeps excluded from autocorrelation and block statistics.
87
+ observable : callable, optional
88
+ Function taking the current ``PackingConfiguration`` and returning one
89
+ finite scalar. The default records mean squared displacement (MSD).
90
+ observable_name : str, default="mean_squared_displacement_m2"
91
+ Label for the scalar observable in the report.
92
+ block_size : int, optional
93
+ Number of post-burn-in samples per block. By default it is selected
94
+ from the estimated autocorrelation time.
95
+ confidence_level : float, default=0.95
96
+ Open-interval normal confidence level for the block-mean estimate.
97
+
98
+ Returns
99
+ -------
100
+ MetropolisDiagnosticReport
101
+ Move deltas, per-sample rates, MSD relative to the starting state, and
102
+ autocorrelation/block summaries. MSD uses accumulated accepted moves,
103
+ including periodic crossings, relative to the start of this call.
104
+ The report does not establish equilibration.
105
+ """
106
+ total = _validate_integer("number_of_sweeps", number_of_sweeps, 1)
107
+ interval = _validate_integer("sample_interval", sample_interval, 1)
108
+ burn_in = _validate_integer("burn_in_sweeps", burn_in_sweeps, 0)
109
+ if burn_in >= total:
110
+ raise ValueError("burn_in_sweeps must be smaller than number_of_sweeps.")
111
+ if not 0.0 < confidence_level < 1.0:
112
+ raise ValueError("confidence_level must lie strictly between 0 and 1.")
113
+ if not callable(getattr(simulator, "run_sweeps", None)):
114
+ raise TypeError("simulator must be a MetropolisSimulator exposing run_sweeps().")
115
+
116
+ start_displacement = np.asarray(
117
+ simulator.cumulative_displacements.to("meter").magnitude, dtype=float
118
+ ).copy()
119
+ previous_accepted = int(simulator.statistics.accepted_moves)
120
+ previous_rejected = int(simulator.statistics.rejected_moves)
121
+ sweeps: list[int] = []
122
+ rates: list[float] = []
123
+ accepted: list[int] = []
124
+ rejected: list[int] = []
125
+ msd: list[float] = []
126
+ observations: list[float] = []
127
+ completed = 0
128
+ final_result = None
129
+ while completed < total:
130
+ chunk = min(interval, total - completed)
131
+ final_result = simulator.run_sweeps(chunk)
132
+ completed += chunk
133
+ current_statistics = simulator.statistics
134
+ accepted_delta = current_statistics.accepted_moves - previous_accepted
135
+ rejected_delta = current_statistics.rejected_moves - previous_rejected
136
+ attempted_delta = accepted_delta + rejected_delta
137
+ displacement = np.asarray(
138
+ simulator.cumulative_displacements.to("meter").magnitude, dtype=float
139
+ ) - start_displacement
140
+ current_msd = float(
141
+ np.mean(np.einsum("ij,ij->i", displacement, displacement))
142
+ )
143
+ value = (
144
+ current_msd if observable is None else float(observable(simulator.sphere_configuration))
145
+ )
146
+ if not np.isfinite(value):
147
+ raise ValueError("observable must return a finite scalar.")
148
+ sweeps.append(completed)
149
+ rates.append(accepted_delta / attempted_delta if attempted_delta else 0.0)
150
+ accepted.append(accepted_delta)
151
+ rejected.append(rejected_delta)
152
+ msd.append(current_msd)
153
+ observations.append(value)
154
+ previous_accepted = int(current_statistics.accepted_moves)
155
+ previous_rejected = int(current_statistics.rejected_moves)
156
+
157
+ sample_sweeps = np.asarray(sweeps, dtype=int)
158
+ selected = sample_sweeps > burn_in
159
+ post_burn_values = np.asarray(observations, dtype=float)[selected]
160
+ if post_burn_values.size < 2:
161
+ raise ValueError("burn-in leaves fewer than two retained samples for diagnostics.")
162
+ tau = _autocorrelation_time(post_burn_values)
163
+ ess = post_burn_values.size / (2.0 * tau)
164
+ selected_block_size = (
165
+ int(np.ceil(2.0 * tau))
166
+ if block_size is None
167
+ else _validate_integer("block_size", block_size, 1)
168
+ )
169
+ blocks = [
170
+ post_burn_values[index : index + selected_block_size].mean()
171
+ for index in range(0, post_burn_values.size, selected_block_size)
172
+ if post_burn_values[index : index + selected_block_size].size == selected_block_size
173
+ ]
174
+ if len(blocks) < 2:
175
+ raise ValueError(
176
+ "fewer than two complete blocks remain; reduce block_size or collect more samples."
177
+ )
178
+ block_means = np.asarray(blocks)
179
+ standard_error = float(block_means.std(ddof=1) / np.sqrt(block_means.size))
180
+ z_value = NormalDist().inv_cdf((1.0 + confidence_level) / 2.0)
181
+ mean = float(block_means.mean())
182
+ return MetropolisDiagnosticReport(
183
+ sample_sweeps=sample_sweeps,
184
+ acceptance_rates=np.asarray(rates),
185
+ accepted_move_deltas=np.asarray(accepted, dtype=int),
186
+ rejected_move_deltas=np.asarray(rejected, dtype=int),
187
+ mean_squared_displacements=np.asarray(msd) * ureg.meter**2,
188
+ observable=np.asarray(observations),
189
+ observable_name=observable_name,
190
+ burn_in_sweeps=burn_in,
191
+ autocorrelation_time=tau,
192
+ effective_sample_size=ess,
193
+ block_means=block_means,
194
+ standard_error=standard_error,
195
+ confidence_interval=(mean - z_value * standard_error, mean + z_value * standard_error),
196
+ confidence_level=confidence_level,
197
+ final_result=final_result,
198
+ )
@@ -0,0 +1,30 @@
1
+ from typing import Any, Callable
2
+
3
+ class MetropolisDiagnosticReport:
4
+ sample_sweeps: Any
5
+ acceptance_rates: Any
6
+ accepted_move_deltas: Any
7
+ rejected_move_deltas: Any
8
+ mean_squared_displacements: Any
9
+ observable: Any
10
+ observable_name: str
11
+ burn_in_sweeps: int
12
+ autocorrelation_time: float
13
+ effective_sample_size: float
14
+ block_means: Any
15
+ standard_error: float
16
+ confidence_interval: tuple[float, float]
17
+ confidence_level: float
18
+ final_result: Any
19
+
20
+ def run_metropolis_diagnostics(
21
+ simulator: Any,
22
+ number_of_sweeps: int,
23
+ *,
24
+ sample_interval: int = ...,
25
+ burn_in_sweeps: int = ...,
26
+ observable: Callable[[Any], float] | None = ...,
27
+ observable_name: str = ...,
28
+ block_size: int | None = ...,
29
+ confidence_level: float = ...,
30
+ ) -> MetropolisDiagnosticReport: ...
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+
3
+ class PackingDomain:
4
+ def __init__(
5
+ self, length_x: Any, length_y: Any, length_z: Any, use_periodic_boundaries: bool
6
+ ) -> None: ...
7
+ @property
8
+ def length_x(self) -> Any: ...
9
+ @property
10
+ def length_y(self) -> Any: ...
11
+ @property
12
+ def length_z(self) -> Any: ...
13
+ volume: float
14
+ use_periodic_boundaries: bool
15
+ def scale(self, scale_factor: float) -> None: ...
@@ -0,0 +1,27 @@
1
+ from typing import Any
2
+ from PackLab.monte_carlo.domain import PackingDomain
3
+ from PackLab.monte_carlo.simulator import RSAOptions
4
+ from PackLab.samplers import RadiusSampler
5
+
6
+ class PackingEstimate:
7
+ centers: Any
8
+ mean_g: Any
9
+ std_g: Any
10
+ number_of_species: int
11
+ number_of_bins: int
12
+
13
+ class PackingEstimator:
14
+ def __init__(
15
+ self,
16
+ domain: PackingDomain,
17
+ radius_sampler: RadiusSampler,
18
+ options: RSAOptions,
19
+ number_of_bins: int,
20
+ ) -> None: ...
21
+ def estimate(
22
+ self,
23
+ number_of_samples: int,
24
+ maximum_pairs: int = 0,
25
+ progress: bool = False,
26
+ progress_interval: int = 1,
27
+ ) -> PackingEstimate: ...
@@ -0,0 +1,36 @@
1
+ from typing import Any
2
+
3
+ from PackLab.monte_carlo.domain import PackingDomain
4
+ from PackLab.monte_carlo.simulator import PackingConfiguration
5
+
6
+
7
+ class MetropolisOptions:
8
+ random_seed: int
9
+ number_of_sweeps: int
10
+ maximum_displacement: Any
11
+
12
+
13
+ class MetropolisStatistics:
14
+ attempted_moves: int
15
+ accepted_moves: int
16
+ rejected_moves: int
17
+ completed_sweeps: int
18
+ total_runtime_seconds: float
19
+ acceptance_rate: float
20
+
21
+
22
+ class MetropolisSimulator:
23
+ def __init__(
24
+ self,
25
+ domain: PackingDomain,
26
+ initial_configuration: PackingConfiguration,
27
+ options: MetropolisOptions,
28
+ ) -> None: ...
29
+ sphere_configuration: PackingConfiguration
30
+ domain: PackingDomain
31
+ statistics: MetropolisStatistics
32
+ @property
33
+ def cumulative_displacements(self) -> Any: ...
34
+ def reset(self) -> None: ...
35
+ def run(self) -> Any: ...
36
+ def run_sweeps(self, number_of_sweeps: int) -> Any: ...