tensErr 0.4.0__tar.gz

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.
tenserr-0.4.0/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Alexander Michel
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice,
10
+ this list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,5 @@
1
+ # Include tests with their shared fixtures and mathematical conventions in sdists.
2
+ recursive-include tests *.py *.md
3
+
4
+ # Exclude Jupyter-generated checkpoint copies from source distributions.
5
+ global-exclude .ipynb_checkpoints/*
tenserr-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: tensErr
3
+ Version: 0.4.0
4
+ Summary: Torch implementation of dense UWerr gamma-method mean estimates.
5
+ License-Expression: BSD-3-Clause
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: torch
10
+ Dynamic: license-file
11
+
12
+ [//]:<This document targets Github's markdown renderer; do not remove this comment.>
13
+ # tensErr
14
+
15
+ tensErr computes uncertainties for tensor-valued Markov chain Monte Carlo
16
+ ensembles using the gamma method
17
+ ([Wolff, 2004](https://arxiv.org/abs/hep-lat/0306017),
18
+ [Wolff, 2009](https://arxiv.org/pdf/0812.0677#page=19)).
19
+ It works directly with PyTorch tensors, using the same interface on CPUs
20
+ and accelerators.
21
+
22
+ ## Gamma method
23
+
24
+ ```python
25
+ def gamma_method(
26
+ samples: torch.Tensor,
27
+ *,
28
+ gamma_method_s: float = 2.0,
29
+ accumulation_dtype: torch.dtype = torch.float64,
30
+ return_autocorrelation: bool = False,
31
+ ) -> GammaMethodEstimate: ...
32
+ ```
33
+
34
+ `samples` may have shape $`(N,)`$ for one chain, $`(R,N)`$ for $`R`$ chains,
35
+ or $`(\ast B,R,N)`$ for batched observables. Every chain must have the same
36
+ retained length $`N\geq2`$.
37
+
38
+ The default `gamma_method_s=2.0` selects the autocorrelation window
39
+ automatically. Setting `gamma_method_s=0.0` disables autocorrelation analysis
40
+ and reports the IID variance of the supplied entries.
41
+
42
+ `gamma_method` returns a `GammaMethodEstimate` with the following fields:
43
+
44
+ | Field | Meaning |
45
+ | --- | --- |
46
+ | `value` | Mean of the supplied samples |
47
+ | `stderr` | Standard error of `value` |
48
+ | `snr` | $`\lvert\mathrm{value}\rvert/\mathrm{stderr}`$ if $`\mathrm{stderr}\gt0`$; $`+\infty`$ if $`\mathrm{stderr}=0`$ |
49
+ | `tau_int` | Integrated autocorrelation time $`\tau_{\mathrm{int}}`$ |
50
+ | `C_f` | Summed autocovariance $`C_f=2\tau_{\mathrm{int}}v_f`$, where $`v_f=\Gamma_f(0)`$ |
51
+ | `stderr_of_stderr` | Estimated uncertainty of `stderr` |
52
+ | `window` | Selected maximum lag |
53
+ | `sample_shapes` | Retained length of every replica |
54
+ | `autocovariance` | $`\Gamma_f(t)`$ with shape $`(\ast B,\lfloor N/2\rfloor)`$ when requested; otherwise `None` |
55
+ | `autocorrelation` | $`\rho_f(t)`$ with shape $`(\ast B,\lfloor N/2\rfloor)`$ when requested; otherwise `None` |
56
+
57
+ `GammaMethodEstimate` objects representing the same observable but arising from independent samples can be averaged to produce one `GammaMethodEstimate` using the overloaded `+` and `sum` operators. Error analysis can thus be performed using chunking, without loading all the samples simultaneously into memory. Combining estimates concatenates `sample_shapes`; `stderr_of_stderr`, `window`,
58
+ `autocovariance`, and `autocorrelation` are `None` on the result.
59
+
60
+ ## Efficient norm and error for autocorrelated vectors
61
+
62
+ Documentation under construction.
@@ -0,0 +1,51 @@
1
+ [//]:<This document targets Github's markdown renderer; do not remove this comment.>
2
+ # tensErr
3
+
4
+ tensErr computes uncertainties for tensor-valued Markov chain Monte Carlo
5
+ ensembles using the gamma method
6
+ ([Wolff, 2004](https://arxiv.org/abs/hep-lat/0306017),
7
+ [Wolff, 2009](https://arxiv.org/pdf/0812.0677#page=19)).
8
+ It works directly with PyTorch tensors, using the same interface on CPUs
9
+ and accelerators.
10
+
11
+ ## Gamma method
12
+
13
+ ```python
14
+ def gamma_method(
15
+ samples: torch.Tensor,
16
+ *,
17
+ gamma_method_s: float = 2.0,
18
+ accumulation_dtype: torch.dtype = torch.float64,
19
+ return_autocorrelation: bool = False,
20
+ ) -> GammaMethodEstimate: ...
21
+ ```
22
+
23
+ `samples` may have shape $`(N,)`$ for one chain, $`(R,N)`$ for $`R`$ chains,
24
+ or $`(\ast B,R,N)`$ for batched observables. Every chain must have the same
25
+ retained length $`N\geq2`$.
26
+
27
+ The default `gamma_method_s=2.0` selects the autocorrelation window
28
+ automatically. Setting `gamma_method_s=0.0` disables autocorrelation analysis
29
+ and reports the IID variance of the supplied entries.
30
+
31
+ `gamma_method` returns a `GammaMethodEstimate` with the following fields:
32
+
33
+ | Field | Meaning |
34
+ | --- | --- |
35
+ | `value` | Mean of the supplied samples |
36
+ | `stderr` | Standard error of `value` |
37
+ | `snr` | $`\lvert\mathrm{value}\rvert/\mathrm{stderr}`$ if $`\mathrm{stderr}\gt0`$; $`+\infty`$ if $`\mathrm{stderr}=0`$ |
38
+ | `tau_int` | Integrated autocorrelation time $`\tau_{\mathrm{int}}`$ |
39
+ | `C_f` | Summed autocovariance $`C_f=2\tau_{\mathrm{int}}v_f`$, where $`v_f=\Gamma_f(0)`$ |
40
+ | `stderr_of_stderr` | Estimated uncertainty of `stderr` |
41
+ | `window` | Selected maximum lag |
42
+ | `sample_shapes` | Retained length of every replica |
43
+ | `autocovariance` | $`\Gamma_f(t)`$ with shape $`(\ast B,\lfloor N/2\rfloor)`$ when requested; otherwise `None` |
44
+ | `autocorrelation` | $`\rho_f(t)`$ with shape $`(\ast B,\lfloor N/2\rfloor)`$ when requested; otherwise `None` |
45
+
46
+ `GammaMethodEstimate` objects representing the same observable but arising from independent samples can be averaged to produce one `GammaMethodEstimate` using the overloaded `+` and `sum` operators. Error analysis can thus be performed using chunking, without loading all the samples simultaneously into memory. Combining estimates concatenates `sample_shapes`; `stderr_of_stderr`, `window`,
47
+ `autocovariance`, and `autocorrelation` are `None` on the result.
48
+
49
+ ## Efficient norm and error for autocorrelated vectors
50
+
51
+ Documentation under construction.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tensErr"
7
+ version = "0.4.0"
8
+ description = "Torch implementation of dense UWerr gamma-method mean estimates."
9
+ readme = "README.md"
10
+ license = "BSD-3-Clause"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ dependencies = ["torch"]
14
+
15
+ [dependency-groups]
16
+ dev = [
17
+ "pyerrors>=2.17",
18
+ "pytest>=8.0",
19
+ ]
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["src"]
23
+ include = ["tensErr*"]
24
+
25
+ [tool.setuptools.package-data]
26
+ tensErr = ["py.typed"]
27
+
28
+ [tool.pytest.ini_options]
29
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ """Public API for tensErr gamma-method and vector-norm estimates."""
2
+
3
+ from tensErr.estimate import Estimate
4
+ from tensErr.gamma_method import (
5
+ GammaMethodEstimate,
6
+ gamma_method,
7
+ )
8
+ from tensErr.vector_norm_gamma_method import (
9
+ VectorNormGammaMethodEstimate,
10
+ VectorNormGammaMethodHelper,
11
+ vector_norm_gamma_method,
12
+ )
13
+
14
+ # __all__ lists the stable public names exported by tensErr.
15
+ __all__ = [
16
+ "Estimate",
17
+ "GammaMethodEstimate",
18
+ "VectorNormGammaMethodHelper",
19
+ "VectorNormGammaMethodEstimate",
20
+ "gamma_method",
21
+ "vector_norm_gamma_method",
22
+ ]
@@ -0,0 +1,33 @@
1
+ """Shared tensor estimate interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ import torch
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class Estimate:
12
+ """Estimate stores a tensor value and its nonnegative standard error.
13
+
14
+ ``stderr`` satisfies the invariant ``stderr >= 0``. The derived ``snr`` field
15
+ represents zero reported noise as positive infinity, independently of
16
+ ``value``.
17
+ """
18
+
19
+ value: torch.Tensor
20
+ stderr: torch.Tensor
21
+ snr: torch.Tensor = field(init=False)
22
+
23
+ def __post_init__(self) -> None:
24
+ """Derive ``snr`` once from ``value`` and ``stderr`` after initialization."""
25
+ object.__setattr__(
26
+ self,
27
+ "snr",
28
+ torch.where(
29
+ self.stderr > 0,
30
+ torch.abs(self.value) / self.stderr,
31
+ torch.inf,
32
+ ),
33
+ )
@@ -0,0 +1,237 @@
1
+ """Dense torch implementation of pyerrors-style gamma-method means."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, replace
6
+
7
+ import torch
8
+
9
+ from tensErr.estimate import Estimate
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class GammaMethodEstimate(Estimate):
14
+ """Batch-shaped gamma-method estimates with per-replica sample lengths.
15
+
16
+ ``C_f`` is the windowed summed autocovariance ``2 * tau_int * v_f``,
17
+ where ``v_f`` is the lag-zero autocovariance.
18
+ """
19
+
20
+ tau_int: torch.Tensor
21
+ C_f: torch.Tensor
22
+ sample_shapes: torch.Tensor
23
+ stderr_of_stderr: torch.Tensor | None
24
+ window: torch.Tensor | None
25
+ autocovariance: torch.Tensor | None = None # Γ(t)
26
+ autocorrelation: torch.Tensor | None = None # ⍴(t)
27
+
28
+ def __add__(self, other: object) -> GammaMethodEstimate:
29
+ """Return the sample-count-weighted combination of ``self`` and ``other``."""
30
+ if not isinstance(other, GammaMethodEstimate) or type(self) is not type(other):
31
+ return NotImplemented
32
+
33
+ self_batch_shapes = (self.value.shape, self.stderr.shape, self.tau_int.shape)
34
+ other_batch_shapes = (other.value.shape, other.stderr.shape, other.tau_int.shape)
35
+ if len(set(self_batch_shapes + other_batch_shapes)) != 1:
36
+ raise ValueError(
37
+ "GammaMethodEstimate batch shapes must match for addition; "
38
+ f"got self={self_batch_shapes} and other={other_batch_shapes}"
39
+ )
40
+
41
+ self_sample_count = self.sample_shapes.sum().to(dtype=self.value.dtype)
42
+ other_sample_count = other.sample_shapes.sum().to(dtype=other.value.dtype)
43
+ total_sample_count = self_sample_count + other_sample_count
44
+ value = (
45
+ self_sample_count * self.value
46
+ + other_sample_count * other.value
47
+ ) / total_sample_count
48
+ stderr = torch.sqrt(
49
+ (self_sample_count * self.stderr).square()
50
+ + (other_sample_count * other.stderr).square()
51
+ ) / total_sample_count
52
+ tau_int = (
53
+ self_sample_count * self.tau_int
54
+ + other_sample_count * other.tau_int
55
+ ) / total_sample_count
56
+ C_f = (
57
+ self_sample_count * self.C_f
58
+ + other_sample_count * other.C_f
59
+ ) / total_sample_count
60
+ return replace(
61
+ self,
62
+ value=value,
63
+ stderr=stderr,
64
+ tau_int=tau_int,
65
+ C_f=C_f,
66
+ sample_shapes=torch.cat((self.sample_shapes, other.sample_shapes)),
67
+ stderr_of_stderr=None,
68
+ window=None,
69
+ autocovariance=None,
70
+ autocorrelation=None,
71
+ **self._addition_field_updates(other),
72
+ )
73
+
74
+ def _addition_field_updates(
75
+ self,
76
+ other: GammaMethodEstimate,
77
+ ) -> dict[str, object]:
78
+ """Return subclass field updates required when combining with ``other``."""
79
+ return {}
80
+
81
+ def __radd__(self, other: object) -> GammaMethodEstimate:
82
+ """Return ``self`` for Python ``sum``'s zero start or defer addition."""
83
+ if isinstance(other, int) and other == 0:
84
+ return self
85
+ if isinstance(other, GammaMethodEstimate):
86
+ return other.__add__(self)
87
+ return NotImplemented
88
+
89
+
90
+ def _pooled_autocovariance(centered: torch.Tensor, w_max: int) -> torch.Tensor:
91
+ """Return pooled dense-chain autocovariance for lags [0, w_max).
92
+
93
+ Replica power spectra are summed before the inverse transform because its
94
+ linearity avoids materializing an inverse-FFT output for every replica.
95
+ """
96
+ sample_count = centered.shape[-1]
97
+ chain_count = centered.shape[-2]
98
+ padding = sample_count + w_max + (sample_count + w_max) % 2
99
+ spectrum = torch.fft.rfft(centered, n=padding, dim=-1)
100
+ pooled_power = spectrum.abs().square().sum(dim=-2)
101
+ lag_sums = torch.fft.irfft(pooled_power, n=padding, dim=-1)[..., :w_max]
102
+ pair_counts = torch.arange(
103
+ sample_count,
104
+ sample_count - w_max,
105
+ -1,
106
+ dtype=centered.dtype,
107
+ device=centered.device,
108
+ )
109
+ return lag_sums / (chain_count * pair_counts)
110
+
111
+
112
+ def _safe_autocorrelation(autocovariance: torch.Tensor) -> torch.Tensor:
113
+ """Return autocorrelation while keeping zero-variance histories finite."""
114
+ variance = autocovariance[..., :1]
115
+ return torch.where(
116
+ variance != 0,
117
+ autocovariance / variance,
118
+ torch.zeros_like(autocovariance),
119
+ )
120
+
121
+
122
+ def _tau_summation_history(autocorrelation: torch.Tensor) -> torch.Tensor:
123
+ """Return pyerrors cumulative tau_int history for autocorrelation lags."""
124
+ leading_half = torch.full_like(autocorrelation[..., :1], 0.5)
125
+ τ = torch.cumsum(
126
+ torch.cat((leading_half, autocorrelation[..., 1:]), dim=-1),
127
+ dim=-1,
128
+ )
129
+ return torch.maximum(τ, torch.full_like(τ, 0.5 + torch.finfo(τ.dtype).eps))
130
+
131
+
132
+ def _automatic_window(
133
+ τ_history: torch.Tensor,
134
+ total_count: int,
135
+ gamma_method_s: float,
136
+ ) -> torch.Tensor:
137
+ """Return pyerrors automatic-window indices for τ_history and S."""
138
+ w_max = τ_history.shape[-1]
139
+ if w_max <= 1:
140
+ return torch.zeros(τ_history.shape[:-1], dtype=torch.long, device=τ_history.device)
141
+
142
+ candidates = torch.arange(1, w_max, dtype=τ_history.dtype, device=τ_history.device)
143
+ τ_tail = gamma_method_s / torch.log(
144
+ (2 * τ_history[..., 1:] + 1) / (2 * τ_history[..., 1:] - 1)
145
+ )
146
+ g_w = torch.exp(-candidates / τ_tail) - τ_tail / torch.sqrt(candidates * total_count)
147
+ below_zero = g_w < 0
148
+ below_zero[..., -1] = True
149
+ return below_zero.to(torch.long).argmax(dim=-1) + 1
150
+
151
+
152
+ def _gather_lag(history: torch.Tensor, window: torch.Tensor) -> torch.Tensor:
153
+ """Return history entries selected by the per-batch window tensor."""
154
+ return torch.gather(history, dim=-1, index=window.unsqueeze(-1)).squeeze(-1)
155
+
156
+
157
+ def gamma_method(
158
+ samples: torch.Tensor,
159
+ *,
160
+ gamma_method_s: float = 2.0,
161
+ accumulation_dtype: torch.dtype = torch.float64,
162
+ return_autocorrelation: bool = False,
163
+ ) -> GammaMethodEstimate:
164
+ """Estimate a dense-history mean and pyerrors-style gamma-method error.
165
+
166
+ samples is interpreted as (..., chains, samples), except a one-dimensional
167
+ input is treated as one unbatched chain. The returned value, stderr, tau_int,
168
+ C_f, stderr_of_stderr, and window tensors have the leading batch shape,
169
+ while the optional autocovariance and autocorrelation tensors have shape
170
+ (*batch_shape, N // 2).
171
+ """
172
+ x = samples.to(dtype=accumulation_dtype)
173
+ if x.ndim == 1:
174
+ x = x.unsqueeze(0)
175
+ sample_count = x.shape[-1]
176
+ chain_count = x.shape[-2]
177
+ total_count = chain_count * sample_count
178
+ w_max = sample_count // 2
179
+
180
+ value = x.mean(dim=(-2, -1))
181
+ centered = x - x.mean(dim=-1, keepdim=True)
182
+ autocovariance = _pooled_autocovariance(centered, w_max)
183
+ autocorrelation = _safe_autocorrelation(autocovariance)
184
+ v_f = autocovariance[..., 0]
185
+ zero_variance = v_f == 0
186
+
187
+ if gamma_method_s == 0.0:
188
+ window = torch.zeros_like(v_f, dtype=torch.long)
189
+ tau_int = torch.full_like(v_f, 0.5)
190
+ C_f = 2 * tau_int * v_f
191
+ stderr = torch.sqrt(C_f / (total_count - 1))
192
+ stderr_of_stderr = stderr * torch.sqrt(
193
+ torch.as_tensor(0.5 / total_count, dtype=x.dtype, device=x.device)
194
+ )
195
+ else:
196
+ τ_history = _tau_summation_history(autocorrelation)
197
+ window = _automatic_window(τ_history, total_count, gamma_method_s)
198
+ window_float = window.to(dtype=x.dtype)
199
+ τ_window = _gather_lag(τ_history, window)
200
+ bias = (1 + (2 * window_float + 1) / total_count) / (1 + 1 / total_count)
201
+ tau_int = τ_window * bias
202
+ C_f = 2 * tau_int * v_f
203
+ stderr = torch.sqrt(C_f * (1 + 1 / total_count) / total_count)
204
+ stderr_of_stderr = stderr * torch.sqrt((window_float + 0.5) / total_count)
205
+
206
+ tau_int = torch.where(zero_variance, torch.full_like(tau_int, 0.5), tau_int)
207
+ stderr = torch.where(zero_variance, torch.zeros_like(stderr), stderr)
208
+ stderr_of_stderr = torch.where(
209
+ zero_variance,
210
+ torch.zeros_like(stderr_of_stderr),
211
+ stderr_of_stderr,
212
+ )
213
+ window = torch.where(zero_variance, torch.zeros_like(window), window)
214
+
215
+ if return_autocorrelation:
216
+ returned_autocovariance = autocovariance
217
+ returned_autocorrelation = autocorrelation
218
+ else:
219
+ returned_autocovariance = None
220
+ returned_autocorrelation = None
221
+
222
+ return GammaMethodEstimate(
223
+ value=value,
224
+ stderr=stderr,
225
+ tau_int=tau_int,
226
+ C_f=C_f,
227
+ sample_shapes=torch.full(
228
+ (chain_count,),
229
+ sample_count,
230
+ dtype=torch.long,
231
+ device=x.device,
232
+ ),
233
+ stderr_of_stderr=stderr_of_stderr,
234
+ window=window,
235
+ autocovariance=returned_autocovariance,
236
+ autocorrelation=returned_autocorrelation,
237
+ )
@@ -0,0 +1 @@
1
+