tensor-ee 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tensor_ee/__init__.py +28 -0
- tensor_ee/_swin_parameters.py +304 -0
- tensor_ee/blocks/__init__.py +0 -0
- tensor_ee/blocks/raster/__init__.py +36 -0
- tensor_ee/blocks/raster/activations.py +183 -0
- tensor_ee/blocks/raster/conv.py +183 -0
- tensor_ee/blocks/raster/merge.py +66 -0
- tensor_ee/blocks/raster/outputs.py +211 -0
- tensor_ee/blocks/raster/pooling.py +182 -0
- tensor_ee/blocks/raster/resample.py +64 -0
- tensor_ee/blocks/window/__init__.py +44 -0
- tensor_ee/blocks/window/activations.py +54 -0
- tensor_ee/blocks/window/attention.py +366 -0
- tensor_ee/blocks/window/conv.py +323 -0
- tensor_ee/blocks/window/linear.py +129 -0
- tensor_ee/blocks/window/merge.py +67 -0
- tensor_ee/blocks/window/normalization.py +197 -0
- tensor_ee/blocks/window/patch.py +362 -0
- tensor_ee/blocks/window/pooling.py +275 -0
- tensor_ee/blocks/window/resample.py +286 -0
- tensor_ee/blocks/window/segformer.py +718 -0
- tensor_ee/blocks/window/swin.py +602 -0
- tensor_ee/blocks/window/swin_upernet_config.py +361 -0
- tensor_ee/blocks/window/upernet.py +1016 -0
- tensor_ee/comparison.py +213 -0
- tensor_ee/compatibility.py +180 -0
- tensor_ee/compiler.py +356 -0
- tensor_ee/converter/__init__.py +72 -0
- tensor_ee/converter/build.py +120 -0
- tensor_ee/converter/convert.py +388 -0
- tensor_ee/converter/export.py +846 -0
- tensor_ee/converter/graph.py +417 -0
- tensor_ee/converter/operations.py +790 -0
- tensor_ee/converter/plan.py +675 -0
- tensor_ee/converter/recognizers.py +1273 -0
- tensor_ee/converter/request.py +146 -0
- tensor_ee/converter/run.py +950 -0
- tensor_ee/core/__init__.py +8 -0
- tensor_ee/core/errors.py +25 -0
- tensor_ee/core/grid.py +323 -0
- tensor_ee/core/metadata.py +398 -0
- tensor_ee/core/ops.py +336 -0
- tensor_ee/core/precision.py +133 -0
- tensor_ee/core/tensor.py +129 -0
- tensor_ee/examples/__init__.py +1 -0
- tensor_ee/examples/nonwater.py +251 -0
- tensor_ee/layouts/__init__.py +17 -0
- tensor_ee/layouts/boundaries.py +103 -0
- tensor_ee/layouts/raster.py +117 -0
- tensor_ee/layouts/shift.py +226 -0
- tensor_ee/layouts/window.py +201 -0
- tensor_ee/models/__init__.py +16 -0
- tensor_ee/models/fixtures.py +119 -0
- tensor_ee/models/segformer_b0/__init__.py +9 -0
- tensor_ee/models/segformer_b0/configuration.py +209 -0
- tensor_ee/models/segformer_b0/decoder.py +130 -0
- tensor_ee/models/segformer_b0/encoder.py +90 -0
- tensor_ee/models/swin_t224/__init__.py +30 -0
- tensor_ee/models/swin_t224/configuration.py +103 -0
- tensor_ee/models/swin_t224/decoder.py +53 -0
- tensor_ee/models/swin_t224/encoder.py +50 -0
- tensor_ee/onnx_import.py +431 -0
- tensor_ee/outputs.py +16 -0
- tensor_ee/primitives.py +110 -0
- tensor_ee/receptive_field.py +257 -0
- tensor_ee/resnet_models.py +185 -0
- tensor_ee/segwater_inference.py +235 -0
- tensor_ee/segwater_preprocessing.py +182 -0
- tensor_ee/segwater_pytorch.py +249 -0
- tensor_ee/semantics.py +389 -0
- tensor_ee/smp.py +155 -0
- tensor_ee/swin.py +197 -0
- tensor_ee/swin_workflow.py +470 -0
- tensor_ee/tiling.py +287 -0
- tensor_ee/weight_assets.py +371 -0
- tensor_ee/weight_mapping.py +51 -0
- tensor_ee/weights/__init__.py +14 -0
- tensor_ee/weights/assets.py +160 -0
- tensor_ee/weights/constants.py +156 -0
- tensor_ee-0.1.0.dist-info/METADATA +172 -0
- tensor_ee-0.1.0.dist-info/RECORD +84 -0
- tensor_ee-0.1.0.dist-info/WHEEL +5 -0
- tensor_ee-0.1.0.dist-info/licenses/LICENSE +21 -0
- tensor_ee-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""R convolution: PyTorch ``nn.Conv2d`` as ordered neighbourhood bands and
|
|
2
|
+
``matrixMultiply``.
|
|
3
|
+
|
|
4
|
+
Moved whole from ``tensor_ee.compiler`` (``conv2d``, ``_pair``, ``_names``) and
|
|
5
|
+
``tensor_ee.semantics`` (``conv_options`` with the coefficient checks it uses:
|
|
6
|
+
``_coefficient_field``, ``finite_coefficients``). The old names are re-exports
|
|
7
|
+
of these objects. The errors they raise, ``SemanticUnknown`` and ``reject``
|
|
8
|
+
(here ``_reject``), are in ``tensor_ee.core.errors``.
|
|
9
|
+
|
|
10
|
+
Supported settings (contract section 8): odd kernels with same padding, stride
|
|
11
|
+
1 or 2, dilation 1, groups 1, zero padding, finite float32 coefficients. Other
|
|
12
|
+
strides stay rejected.
|
|
13
|
+
|
|
14
|
+
Precision policy ``float32_operands`` (contract section 7): the weight and bias
|
|
15
|
+
arrays are cast ``.float()`` before ``matrixMultiply``. The node output is
|
|
16
|
+
materialized with ``.toFloat()`` by the caller (``compile_model`` or the
|
|
17
|
+
``tensor_ee.primitives`` facade), not here.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
import ee
|
|
25
|
+
import torch
|
|
26
|
+
from torch import nn
|
|
27
|
+
|
|
28
|
+
from ...core.errors import SemanticUnknown, UnsupportedOperation, reject as _reject
|
|
29
|
+
from ...core.metadata import ANY_SETTINGS, BlockMetadata, Locality, validate_metadata
|
|
30
|
+
from ...core.precision import FLOAT32_OPERANDS, validate_policy
|
|
31
|
+
from ...core.tensor import Raster
|
|
32
|
+
# Library converters and coefficient transport. These module-level bindings are
|
|
33
|
+
# the names conv2d looks up (tests patch _inline_float32_array here).
|
|
34
|
+
from ...layouts.raster import project as _project, stride_sample as _stride_sample
|
|
35
|
+
from ...layouts.raster import zero_extended as _zero_extended
|
|
36
|
+
from ...weights.constants import inline_float32_array as _inline_float32_array
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _pair(value: Any) -> tuple[Any, Any]:
|
|
40
|
+
if isinstance(value, (tuple, list)):
|
|
41
|
+
if len(value) != 2:
|
|
42
|
+
raise UnsupportedOperation(f"Expected a scalar or pair, received {value!r}")
|
|
43
|
+
return tuple(value)
|
|
44
|
+
return value, value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _names(count: int) -> tuple[str, ...]:
|
|
48
|
+
return tuple(f"c{i}" for i in range(count))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def finite_coefficients(module: nn.Module) -> None:
|
|
52
|
+
"""Inspect local coefficients only, never an input-shaped activation."""
|
|
53
|
+
for name, value in (*module.named_parameters(recurse=False), *module.named_buffers(recurse=False)):
|
|
54
|
+
if not value.is_floating_point():
|
|
55
|
+
continue
|
|
56
|
+
if value.dtype != torch.float32:
|
|
57
|
+
_reject("Only float32 model parameters and buffers are supported; convert with model.float()", "model_dtype")
|
|
58
|
+
try:
|
|
59
|
+
finite = bool(torch.isfinite(value.detach()).all().item())
|
|
60
|
+
except Exception as error:
|
|
61
|
+
raise SemanticUnknown(f"Cannot inspect local coefficient {name}: {type(error).__name__}: {error}") from error
|
|
62
|
+
if not finite:
|
|
63
|
+
_reject(f"Local coefficient {name} must contain only finite tensors", "nonfinite_coefficients")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _coefficient_field(value: Any, name: str, shape: tuple[int, ...]) -> None:
|
|
67
|
+
# Computational tensors require float32 even though the report's global
|
|
68
|
+
# floating-dtype Boolean deliberately ignores integer bookkeeping.
|
|
69
|
+
if not isinstance(value, torch.Tensor) or value.dtype != torch.float32:
|
|
70
|
+
_reject(f"Only float32 computational coefficient {name} is supported", "model_dtype")
|
|
71
|
+
try:
|
|
72
|
+
actual = tuple(value.shape)
|
|
73
|
+
except Exception as error:
|
|
74
|
+
raise SemanticUnknown(f"Cannot inspect local coefficient {name} shape: {error}") from error
|
|
75
|
+
if actual != shape:
|
|
76
|
+
_reject(f"Local coefficient {name} shape must be {shape}, received {actual}", "coefficient_shape")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def conv_options(module: nn.Conv2d) -> tuple[tuple[int, int], int]:
|
|
80
|
+
kh, kw = _pair(module.kernel_size)
|
|
81
|
+
if kh % 2 != 1 or kw % 2 != 1:
|
|
82
|
+
_reject("Conv2d kernels must have odd height and width")
|
|
83
|
+
padding = (kh // 2, kw // 2) if module.padding == "same" else module.padding
|
|
84
|
+
if padding != (kh // 2, kw // 2):
|
|
85
|
+
_reject("Conv2d must use same padding (kernel_size // 2)")
|
|
86
|
+
stride = _pair(module.stride)
|
|
87
|
+
if (stride not in ((1, 1), (2, 2)) or _pair(module.dilation) != (1, 1)
|
|
88
|
+
or module.groups != 1 or module.padding_mode != "zeros"):
|
|
89
|
+
_reject("Conv2d requires stride=1 or 2, dilation=1, groups=1, padding_mode='zeros'")
|
|
90
|
+
_coefficient_field(module.weight, "Conv2d.weight", (module.out_channels, module.in_channels, kh, kw))
|
|
91
|
+
if module.bias is not None:
|
|
92
|
+
_coefficient_field(module.bias, "Conv2d.bias", (module.out_channels,))
|
|
93
|
+
finite_coefficients(module)
|
|
94
|
+
return (kh, kw), stride[0]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def conv2d(raster: Raster, module: nn.Conv2d,
|
|
98
|
+
parameters: tuple[ee.Array, ee.Array | None] | None = None,
|
|
99
|
+
*, execution_dtype: str = "float32", clip_footprints: bool = False,
|
|
100
|
+
policy: str = FLOAT32_OPERANDS) -> Raster:
|
|
101
|
+
"""PyTorch cross-correlation via ordered neighborhood bands and matrixMultiply."""
|
|
102
|
+
validate_policy(policy, allowed=FLOAT32_OPERANDS)
|
|
103
|
+
if execution_dtype != "float32":
|
|
104
|
+
raise ValueError("execution_dtype must be float32 (maximum supported precision)")
|
|
105
|
+
(kh, kw), stride_value = conv_options(module)
|
|
106
|
+
stride = (stride_value, stride_value)
|
|
107
|
+
if module.in_channels != len(raster.bands):
|
|
108
|
+
raise UnsupportedOperation("Conv2d input channel count does not match raster")
|
|
109
|
+
kernel = ee.Kernel.fixed(kw, kh, [[1] * kw for _ in range(kh)], kw // 2, kh // 2, False)
|
|
110
|
+
# Never rely on the EE-generated default band ordering. Torch's flattened
|
|
111
|
+
# OIHW weights require channels, then rows, then columns. EE suffixes use
|
|
112
|
+
# offsets relative to the kernel origin, with x before y.
|
|
113
|
+
patch_bands = [f"{band}_{dx}_{dy}" for band in raster.bands
|
|
114
|
+
for dy in range(-(kh // 2), kh // 2 + 1)
|
|
115
|
+
for dx in range(-(kw // 2), kw // 2 + 1)]
|
|
116
|
+
patch = (_zero_extended(raster).neighborhoodToBands(kernel)
|
|
117
|
+
.select(patch_bands).toArray().toArray(1))
|
|
118
|
+
if parameters is None:
|
|
119
|
+
weights = _inline_float32_array(module.weight.detach().cpu().float()
|
|
120
|
+
.reshape(module.out_channels, -1).tolist())
|
|
121
|
+
bias = (None if module.bias is None else
|
|
122
|
+
_inline_float32_array([[value] for value in module.bias.detach().cpu().float().tolist()]))
|
|
123
|
+
else:
|
|
124
|
+
weights, bias = parameters
|
|
125
|
+
if execution_dtype == "float32":
|
|
126
|
+
# Keep matrix operands in float32 rather than promoting the entire
|
|
127
|
+
# activation neighborhood to double through the frozen weights.
|
|
128
|
+
weights = weights.float()
|
|
129
|
+
if bias is not None:
|
|
130
|
+
bias = bias.float()
|
|
131
|
+
result = ee.Image(weights).matrixMultiply(patch)
|
|
132
|
+
if bias is not None:
|
|
133
|
+
result = result.add(ee.Image(bias))
|
|
134
|
+
bands = _names(module.out_channels)
|
|
135
|
+
result = result.arrayProject([0]).arrayFlatten([list(bands)])
|
|
136
|
+
# Make the dense convolution's sampling grid explicit before decimation.
|
|
137
|
+
# Stride one already receives that projection in _finish; avoid a second
|
|
138
|
+
# redundant reproject in the long residual/decoder graph.
|
|
139
|
+
if stride[0] > 1:
|
|
140
|
+
result = _project(result, raster.grid, clip_footprints)
|
|
141
|
+
return _stride_sample(result, bands, raster.grid, stride[0], clip_footprints)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Locality and cost metadata (contract section 9)
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
def _conv_coefficients(module: nn.Conv2d) -> int:
|
|
148
|
+
"""Values embedded in the request when the coefficients are inline:
|
|
149
|
+
``out * in * kh * kw`` weights plus ``out`` biases."""
|
|
150
|
+
kh, kw = _pair(module.kernel_size)
|
|
151
|
+
count = module.out_channels * module.in_channels * kh * kw
|
|
152
|
+
return count + (module.out_channels if module.bias is not None else 0)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _conv_asset_arrays(module: nn.Conv2d) -> int:
|
|
156
|
+
"""Server arrays read when the coefficients are supplied as EE arrays:
|
|
157
|
+
the weight matrix, plus the bias column."""
|
|
158
|
+
return 1 + (module.bias is not None)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _no_coefficients(*args, **kwargs) -> int:
|
|
162
|
+
"""Cost of an R block without weights: it embeds no coefficients and reads
|
|
163
|
+
no asset arrays. Scalar options (a slope, a threshold, the float32 maximum)
|
|
164
|
+
are graph literals, not coefficients, and are not counted."""
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
METADATA = validate_metadata({
|
|
169
|
+
# Radius k // 2 for a k x k kernel (receptive_field._window_axis with
|
|
170
|
+
# padding k // 2). The static field records the largest validated kernel,
|
|
171
|
+
# 7 (radius 3); kernels 1 and 3 have radius 0 and 1.
|
|
172
|
+
# Validated: any accepted call. Kernels 1, 3 and 7 at strides 1 and 2, and
|
|
173
|
+
# clip_footprints both ways (True in resnet18_full64_float32_policy.json), were run;
|
|
174
|
+
# the unqualified "Ready" covers them, and other forms are refused.
|
|
175
|
+
"conv2d": BlockMetadata(
|
|
176
|
+
name="conv2d", layout_in="R", layout_out="R", locality=Locality.KERNEL,
|
|
177
|
+
policy=FLOAT32_OPERANDS, embedded_constants=_conv_coefficients,
|
|
178
|
+
asset_arrays=_conv_asset_arrays, radius=3, status="Ready",
|
|
179
|
+
evidence=("docs/history/cnn/results/parity.json", "docs/history/cnn/results/primitives_float32.json",
|
|
180
|
+
"docs/history/cnn/results/resnet_primitives.json", "docs/history/cnn/results/resnet18_full64_float32_policy.json",
|
|
181
|
+
"docs/history/cnn/results/segwater_js_parity_1x1.json", "docs/history/cnn/results/tiled_resnet18_2x2_parity.json"),
|
|
182
|
+
validated=ANY_SETTINGS),
|
|
183
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""R merges: channel concatenation (``torch.cat`` on dim 1) and residual addition.
|
|
2
|
+
|
|
3
|
+
Moved whole from ``tensor_ee.compiler`` (``concatenate``, ``residual_add``).
|
|
4
|
+
The old names are re-exports of these objects. Both require identical grids;
|
|
5
|
+
there is no broadcasting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Sequence
|
|
11
|
+
|
|
12
|
+
from ...core.errors import UnsupportedOperation
|
|
13
|
+
from ...core.metadata import ANY_SETTINGS, BlockMetadata, Locality, Validated, validate_metadata
|
|
14
|
+
from ...core.precision import FLOAT32_OPERANDS, validate_policy
|
|
15
|
+
from ...core.tensor import Raster
|
|
16
|
+
from ...layouts.raster import finish as _finish
|
|
17
|
+
from .conv import _names, _no_coefficients
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def concatenate(rasters: Sequence[Raster], dim: int = 1,
|
|
21
|
+
*, clip_footprints: bool = False, policy: str = FLOAT32_OPERANDS) -> Raster:
|
|
22
|
+
validate_policy(policy, allowed=FLOAT32_OPERANDS)
|
|
23
|
+
if dim not in (1, -3) or not rasters:
|
|
24
|
+
raise UnsupportedOperation("Concatenation requires a nonempty sequence and NCHW channel dim=1")
|
|
25
|
+
grid = rasters[0].grid
|
|
26
|
+
if any(raster.grid != grid for raster in rasters):
|
|
27
|
+
raise UnsupportedOperation("Skip connection tensors must have identical spatial grids")
|
|
28
|
+
parts = [raster.image.rename([f"part{i}_{band}" for band in raster.bands])
|
|
29
|
+
for i, raster in enumerate(rasters)]
|
|
30
|
+
result = parts[0]
|
|
31
|
+
for part in parts[1:]:
|
|
32
|
+
result = result.addBands(part)
|
|
33
|
+
return _finish(result, _names(sum(len(raster.bands) for raster in rasters)), grid, clip_footprints)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def residual_add(left: Raster, right: Raster, *, clip_footprints: bool = False,
|
|
37
|
+
policy: str = FLOAT32_OPERANDS) -> Raster:
|
|
38
|
+
"""Elementwise addition of equally shaped tensors; broadcasting is rejected."""
|
|
39
|
+
validate_policy(policy, allowed=FLOAT32_OPERANDS)
|
|
40
|
+
if not isinstance(left, Raster) or not isinstance(right, Raster):
|
|
41
|
+
raise UnsupportedOperation("Residual addition requires two raster tensors")
|
|
42
|
+
if left.grid != right.grid or len(left.bands) != len(right.bands):
|
|
43
|
+
raise UnsupportedOperation("Residual addition requires identical spatial grids and channel counts")
|
|
44
|
+
return _finish(left.image.add(right.image.rename(list(left.bands))), left.bands, left.grid, clip_footprints)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Locality and cost metadata (contract section 9)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
METADATA = validate_metadata({
|
|
51
|
+
# The U-Net's skip concatenations are torch.cat(dim=1); dim -3, the same axis, never ran.
|
|
52
|
+
"concatenate": BlockMetadata(
|
|
53
|
+
name="concatenate", layout_in="R", layout_out="R", locality=Locality.POINTWISE,
|
|
54
|
+
policy=FLOAT32_OPERANDS, embedded_constants=_no_coefficients,
|
|
55
|
+
asset_arrays=_no_coefficients, status="Ready",
|
|
56
|
+
evidence=("docs/history/cnn/results/parity.json", "docs/history/cnn/results/resnet18_full64_float32_policy.json",
|
|
57
|
+
"docs/history/cnn/results/segwater_js_parity_1x1.json"),
|
|
58
|
+
validated=Validated(({"dim": (1,)},))),
|
|
59
|
+
"residual_add": BlockMetadata(
|
|
60
|
+
name="residual_add", layout_in="R", layout_out="R", locality=Locality.POINTWISE,
|
|
61
|
+
policy=FLOAT32_OPERANDS, embedded_constants=_no_coefficients,
|
|
62
|
+
asset_arrays=_no_coefficients, status="Ready",
|
|
63
|
+
evidence=("docs/history/cnn/results/resnet_primitives.json", "docs/history/cnn/results/resnet18_full64_float32_policy.json",
|
|
64
|
+
"docs/history/cnn/results/segwater_js_parity_1x1.json"),
|
|
65
|
+
validated=ANY_SETTINGS),
|
|
66
|
+
})
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Recipe-free segmentation outputs on explicit finite raster grids.
|
|
2
|
+
|
|
3
|
+
Scores/probabilities are float32. Classes are the ordered raw-logit argmax;
|
|
4
|
+
thresholds compare widened probabilities against a retained binary64 scalar.
|
|
5
|
+
These functions build deferred graphs without cloud inspection or initialization.
|
|
6
|
+
|
|
7
|
+
Moved whole from ``tensor_ee.outputs`` (``segmentation_outputs``,
|
|
8
|
+
``binary_threshold``, ``SegmentationOutputs`` and their helpers). The old names
|
|
9
|
+
are re-exports of these objects. The probabilities are this module's softmax
|
|
10
|
+
block materialized with ``.toFloat()``, exactly as the public
|
|
11
|
+
``tensor_ee.softmax`` primitive the old module called.
|
|
12
|
+
|
|
13
|
+
Precision policy ``float32_operands`` (contract section 7): the threshold
|
|
14
|
+
widens the float32 probabilities ``toDouble()`` and compares them with an exact
|
|
15
|
+
binary64 scalar (``_ee_scalar``). The generic defaults (positive class 1,
|
|
16
|
+
threshold 0.5) are API defaults, not the SegWater recipe (contract section 11).
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from numbers import Integral, Real
|
|
22
|
+
import struct
|
|
23
|
+
from typing import Sequence
|
|
24
|
+
|
|
25
|
+
import ee
|
|
26
|
+
|
|
27
|
+
from ...core.errors import UnsupportedOperation
|
|
28
|
+
from ...core.metadata import ANY_SETTINGS, BlockMetadata, Locality, validate_metadata
|
|
29
|
+
from ...core.precision import FLOAT32_OPERANDS, validate_policy
|
|
30
|
+
from ...core.precision import exact_scalar as _ee_scalar
|
|
31
|
+
from ...core.tensor import Raster
|
|
32
|
+
from ...layouts.raster import _float32, finish as _finish
|
|
33
|
+
from .activations import _real_option, common_score_mask as _common_score_mask, softmax as _softmax
|
|
34
|
+
from .conv import _no_coefficients
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class SegmentationOutputs:
|
|
39
|
+
logits: Raster
|
|
40
|
+
probabilities: Raster
|
|
41
|
+
classes: Raster
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _band_names(names: Sequence[str], count: int, option: str) -> tuple[str, ...]:
|
|
45
|
+
if isinstance(names, str):
|
|
46
|
+
raise UnsupportedOperation(f"{option} must be a sequence of {count} names")
|
|
47
|
+
try:
|
|
48
|
+
result = tuple(names)
|
|
49
|
+
except TypeError as error:
|
|
50
|
+
raise UnsupportedOperation(f"{option} must be a sequence of {count} names") from error
|
|
51
|
+
if (len(result) != count or any(not isinstance(name, str) or not name for name in result)
|
|
52
|
+
or len(set(result)) != count):
|
|
53
|
+
raise UnsupportedOperation(f"{option} must contain exactly {count} nonempty unique names")
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _scores(raster: Raster, *, binary: bool = False) -> ee.Image:
|
|
58
|
+
if not isinstance(raster, Raster):
|
|
59
|
+
raise TypeError("scores must be a Raster")
|
|
60
|
+
count = len(raster.bands)
|
|
61
|
+
if count < 2 or (binary and count != 2):
|
|
62
|
+
requirement = "exactly two" if binary else "at least two"
|
|
63
|
+
raise UnsupportedOperation(f"scores require {requirement} channels")
|
|
64
|
+
_band_names(raster.bands, count, "score bands")
|
|
65
|
+
return raster.image.select(list(raster.bands))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _check_literal_validity(image: ee.Image) -> None:
|
|
69
|
+
"""Inspect only constant literals and rename/cast/reproject wrappers.
|
|
70
|
+
|
|
71
|
+
No generic graph evaluator is attempted. Computed literals, arbitrary graph
|
|
72
|
+
operations and remote assets remain the caller's binary/finiteness assertion.
|
|
73
|
+
A float cast is applied to the literal values before checking the effective
|
|
74
|
+
binary payload; masks/other operations stop this narrow inspection.
|
|
75
|
+
"""
|
|
76
|
+
wrappers = {"Image.rename": "input", "Image.toFloat": "value",
|
|
77
|
+
"Image.toDouble": "value", "Image.reproject": "image"}
|
|
78
|
+
float_cast = False
|
|
79
|
+
while isinstance(image, ee.Image) and image.func is not None:
|
|
80
|
+
signature = image.func.getSignature()
|
|
81
|
+
name = signature.get("name")
|
|
82
|
+
if name not in wrappers:
|
|
83
|
+
break
|
|
84
|
+
float_cast = float_cast or name == "Image.toFloat"
|
|
85
|
+
image = image.args[wrappers[name]]
|
|
86
|
+
if not isinstance(image, ee.Image) or image.func is None:
|
|
87
|
+
return
|
|
88
|
+
if image.func.getSignature().get("name") != "Image.constant":
|
|
89
|
+
return
|
|
90
|
+
value = image.args.get("value")
|
|
91
|
+
if isinstance(value, Real):
|
|
92
|
+
values = (value,)
|
|
93
|
+
elif isinstance(value, (list, tuple)) and all(isinstance(v, Real) for v in value):
|
|
94
|
+
values = tuple(value)
|
|
95
|
+
else:
|
|
96
|
+
return
|
|
97
|
+
if len(values) != 1:
|
|
98
|
+
raise UnsupportedOperation("local validity must have one band matching its Raster metadata")
|
|
99
|
+
if float_cast:
|
|
100
|
+
try:
|
|
101
|
+
values = tuple(struct.unpack("f", struct.pack("f", float(v)))[0] for v in values)
|
|
102
|
+
except (OverflowError, ValueError) as error:
|
|
103
|
+
raise UnsupportedOperation("local validity must contain finite binary values") from error
|
|
104
|
+
if any(v not in (0, 1) for v in values):
|
|
105
|
+
raise UnsupportedOperation("local validity must contain finite binary values")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _validity_mask(validity: Raster | None, raster: Raster) -> ee.Image | None:
|
|
109
|
+
if validity is None:
|
|
110
|
+
return None
|
|
111
|
+
if not isinstance(validity, Raster):
|
|
112
|
+
raise TypeError("validity must be a Raster")
|
|
113
|
+
if validity.grid != raster.grid:
|
|
114
|
+
raise UnsupportedOperation("validity must use the identical output grid")
|
|
115
|
+
_band_names(validity.bands, 1, "validity bands")
|
|
116
|
+
_check_literal_validity(validity.image)
|
|
117
|
+
image = validity.image.select(list(validity.bands))
|
|
118
|
+
return image.eq(1).And(image.mask().gt(0)).unmask(0, sameFootprint=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _masked_scores(raster: Raster, image: ee.Image, validity: Raster | None) -> Raster:
|
|
122
|
+
mask = _common_score_mask(image, raster.grid)
|
|
123
|
+
explicit = _validity_mask(validity, raster)
|
|
124
|
+
if explicit is not None:
|
|
125
|
+
mask = mask.And(explicit)
|
|
126
|
+
return Raster(image.updateMask(mask), raster.bands, raster.grid)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def segmentation_outputs(logits: Raster, *, logit_bands: Sequence[str] | None = None,
|
|
130
|
+
probability_bands: Sequence[str] | None = None,
|
|
131
|
+
class_band: str = "class_index",
|
|
132
|
+
validity: Raster | None = None,
|
|
133
|
+
policy: str = FLOAT32_OPERANDS) -> SegmentationOutputs:
|
|
134
|
+
"""Retain raw logits and return float32 probabilities/int32 logit argmax.
|
|
135
|
+
|
|
136
|
+
Ordered channel indices define classes; exact logit ties choose the first.
|
|
137
|
+
Processed outputs share all score masks/finiteness, finite grid and optional
|
|
138
|
+
explicit validity. Raw logits retain their original values and masks.
|
|
139
|
+
"""
|
|
140
|
+
validate_policy(policy, allowed=FLOAT32_OPERANDS)
|
|
141
|
+
image = _scores(logits)
|
|
142
|
+
count = len(logits.bands)
|
|
143
|
+
logit_names = logits.bands if logit_bands is None else _band_names(logit_bands, count, "logit_bands")
|
|
144
|
+
probability_names = (_band_names(probability_bands, count, "probability_bands") if probability_bands is not None
|
|
145
|
+
else tuple(f"probability_{i}" for i in range(count)))
|
|
146
|
+
class_name = _band_names((class_band,), 1, "class_band")
|
|
147
|
+
raw = logits if logit_bands is None else Raster(image.rename(list(logit_names)), logit_names, logits.grid)
|
|
148
|
+
masked = _masked_scores(logits, image, validity)
|
|
149
|
+
# The softmax block materialized as float32, as tensor_ee.softmax does.
|
|
150
|
+
probabilities = _float32(_softmax(masked))
|
|
151
|
+
probabilities = Raster(probabilities.image.rename(list(probability_names)), probability_names, logits.grid)
|
|
152
|
+
# Do not derive the winner from rounded probabilities: [0,1e-8] can round
|
|
153
|
+
# to [.5,.5]. arrayArgmax returns the first maximum of the ordered logits.
|
|
154
|
+
winner = masked.image.toArray().arrayArgmax().arrayGet([0]).toInt32()
|
|
155
|
+
classes = _finish(winner, class_name, logits.grid)
|
|
156
|
+
return SegmentationOutputs(raw, probabilities, classes)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _threshold_option(threshold: float) -> float:
|
|
160
|
+
if isinstance(threshold, bool) or not isinstance(threshold, Real):
|
|
161
|
+
raise UnsupportedOperation("threshold must be a static finite real scalar")
|
|
162
|
+
if not 0 <= threshold <= 1:
|
|
163
|
+
raise UnsupportedOperation("threshold must satisfy 0 <= threshold <= 1")
|
|
164
|
+
number = _real_option(threshold, "threshold")
|
|
165
|
+
if threshold != 0 and number == 0:
|
|
166
|
+
raise UnsupportedOperation("threshold must be representable in binary64 without underflow to zero")
|
|
167
|
+
return number
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def binary_threshold(probabilities: Raster, *, positive_class: int = 1,
|
|
171
|
+
threshold: float = 0.5, output_band: str = "class_mask",
|
|
172
|
+
validity: Raster | None = None,
|
|
173
|
+
policy: str = FLOAT32_OPERANDS) -> Raster:
|
|
174
|
+
"""Inclusive uint8 threshold on one of exactly two normalized channels.
|
|
175
|
+
|
|
176
|
+
Widen float32 probabilities losslessly and retain the finite binary64
|
|
177
|
+
threshold, so probability .5 is below threshold .50000001. Invalid pixels
|
|
178
|
+
are excluded rather than filled with class zero. Caller supplies normalized
|
|
179
|
+
probabilities and asserts binary/finiteness for computed/remote validity.
|
|
180
|
+
"""
|
|
181
|
+
validate_policy(policy, allowed=FLOAT32_OPERANDS)
|
|
182
|
+
image = _scores(probabilities, binary=True)
|
|
183
|
+
if isinstance(positive_class, bool) or not isinstance(positive_class, Integral) or positive_class not in (0, 1):
|
|
184
|
+
raise UnsupportedOperation("positive_class must be the static index 0 or 1")
|
|
185
|
+
value = _threshold_option(threshold)
|
|
186
|
+
name = _band_names((output_band,), 1, "output_band")
|
|
187
|
+
masked = _masked_scores(probabilities, image, validity)
|
|
188
|
+
decision = masked.image.select(probabilities.bands[int(positive_class)]).toDouble().gte(_ee_scalar(value)).toUint8()
|
|
189
|
+
return _finish(decision, name, probabilities.grid)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Locality and cost metadata (contract section 9)
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
METADATA = validate_metadata({
|
|
196
|
+
# Logits, float32 probabilities and an int32 class band, all on the input grid.
|
|
197
|
+
# Validated: any accepted call (segmentation output validation varied the validity input; band names are labels).
|
|
198
|
+
"segmentation_outputs": BlockMetadata(
|
|
199
|
+
name="segmentation_outputs", layout_in="R", layout_out="R", locality=Locality.POINTWISE,
|
|
200
|
+
policy=FLOAT32_OPERANDS, embedded_constants=_no_coefficients,
|
|
201
|
+
asset_arrays=_no_coefficients, status="Ready",
|
|
202
|
+
evidence=("docs/evidence/t03-outputs.json", "docs/evidence/t05-primary.json"),
|
|
203
|
+
validated=ANY_SETTINGS),
|
|
204
|
+
# Validated: any accepted call (segmentation output validation: thresholds 0, 0.5, 1 and 0.50000001, both classes).
|
|
205
|
+
"binary_threshold": BlockMetadata(
|
|
206
|
+
name="binary_threshold", layout_in="R", layout_out="R", locality=Locality.POINTWISE,
|
|
207
|
+
policy=FLOAT32_OPERANDS, embedded_constants=_no_coefficients,
|
|
208
|
+
asset_arrays=_no_coefficients, status="Ready",
|
|
209
|
+
evidence=("docs/evidence/t03-outputs.json",),
|
|
210
|
+
validated=ANY_SETTINGS),
|
|
211
|
+
})
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""R pooling: PyTorch ``nn.AvgPool2d`` and ``nn.MaxPool2d`` at a small set of
|
|
2
|
+
common CNN configurations.
|
|
3
|
+
|
|
4
|
+
Moved whole from ``tensor_ee.compiler`` (``pool2d``, both branches) and
|
|
5
|
+
``tensor_ee.semantics`` (``pool_options``). The old names are re-exports of
|
|
6
|
+
these objects. The raster fixtures added the configurations beyond the two the reference
|
|
7
|
+
models use (``pool_config``).
|
|
8
|
+
|
|
9
|
+
Supported settings (contract section 8), all with ``ceil_mode=False``,
|
|
10
|
+
max-pool ``dilation=1`` and ``return_indices=False``, and average-pool
|
|
11
|
+
``divisor_override=None``:
|
|
12
|
+
|
|
13
|
+
- **aligned** average or max, kernel = stride = k, padding 0, for k in
|
|
14
|
+
(2, 3, 4), on input sizes divisible by k: ``reduceResolution`` onto the
|
|
15
|
+
k-times coarser grid;
|
|
16
|
+
- **max 3 x 3, padding 1, stride 1 or 2**: a 3 x 3 neighbourhood maximum on
|
|
17
|
+
the input grid (then stride sampling for stride 2). The input is masked
|
|
18
|
+
outside its grid and Earth Engine reducers skip masked pixels, so the padding
|
|
19
|
+
never wins: PyTorch's negative-infinity padding;
|
|
20
|
+
- **average 3 x 3, stride 1, padding 1**, under both PyTorch divisor rules:
|
|
21
|
+
``count_include_pad=True`` divides the in-bounds sum by 9 (zero padding);
|
|
22
|
+
``count_include_pad=False`` takes the mean of the in-bounds pixels only.
|
|
23
|
+
|
|
24
|
+
Every other configuration is rejected with the list above. Each supported
|
|
25
|
+
configuration has a raster-setting validation fixture (``raster/pool2d/*`` in
|
|
26
|
+
``tools/fixtures/raster.py``) that compares it with PyTorch: bit for bit for
|
|
27
|
+
max pooling, within 1e-4 for averages.
|
|
28
|
+
|
|
29
|
+
Precision policy ``float32_operands`` (contract section 7). There are no
|
|
30
|
+
coefficients. The node output is materialized with ``.toFloat()`` by the caller
|
|
31
|
+
(``compile_model`` or the ``tensor_ee.primitives`` facade).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
|
|
38
|
+
import ee
|
|
39
|
+
from torch import nn
|
|
40
|
+
|
|
41
|
+
from ...core.errors import UnsupportedOperation
|
|
42
|
+
from ...core.metadata import BlockMetadata, Locality, Validated, validate_metadata
|
|
43
|
+
from ...core.precision import FLOAT32_OPERANDS, validate_policy
|
|
44
|
+
from ...core.tensor import Raster
|
|
45
|
+
from ...layouts.raster import finish as _finish, project as _project
|
|
46
|
+
from ...layouts.raster import stride_sample as _stride_sample
|
|
47
|
+
from ...core.errors import reject as _reject
|
|
48
|
+
from .conv import _no_coefficients, _pair
|
|
49
|
+
|
|
50
|
+
ALIGNED_KERNELS = (2, 3, 4)
|
|
51
|
+
"""Kernel sizes k of the aligned k x k pools (stride k, padding 0)."""
|
|
52
|
+
|
|
53
|
+
SUPPORTED = ("aligned avg/max k x k with stride k and padding 0 for k in (2, 3, 4) on sizes divisible "
|
|
54
|
+
"by k; max 3x3 padding 1 with stride 1 or 2; avg 3x3 stride 1 padding 1")
|
|
55
|
+
"""The supported configurations, as the rejection message names them."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class PoolConfig:
|
|
60
|
+
"""One supported pooling configuration, normalized from the PyTorch module.
|
|
61
|
+
|
|
62
|
+
``reducer`` is ``"max"`` or ``"mean"``; ``kernel``, ``stride`` and
|
|
63
|
+
``padding`` are the equal per-axis values; ``count_include_pad`` is the
|
|
64
|
+
average pool's divisor rule (``None`` for max pooling).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
reducer: str
|
|
68
|
+
kernel: int
|
|
69
|
+
stride: int
|
|
70
|
+
padding: int
|
|
71
|
+
count_include_pad: bool | None = None
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def aligned(self) -> bool:
|
|
75
|
+
"""An aligned k x k pool: kernel = stride, no padding."""
|
|
76
|
+
return self.padding == 0
|
|
77
|
+
|
|
78
|
+
def output_size(self, size: int) -> int:
|
|
79
|
+
"""PyTorch's output size on one axis (``ceil_mode=False``)."""
|
|
80
|
+
return (size + 2 * self.padding - self.kernel) // self.stride + 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def aligned_size_message(kernel: int) -> str:
|
|
84
|
+
"""The rejection of an input size that an aligned k x k pool does not divide."""
|
|
85
|
+
if kernel == 2:
|
|
86
|
+
return "This experiment requires even spatial dimensions before pooling"
|
|
87
|
+
return f"Aligned {kernel}x{kernel} pooling requires spatial dimensions divisible by {kernel}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pool_config(module: nn.AvgPool2d | nn.MaxPool2d) -> PoolConfig:
|
|
91
|
+
"""Validate the module's options and return its supported configuration."""
|
|
92
|
+
stride = module.stride if module.stride is not None else module.kernel_size
|
|
93
|
+
if module.ceil_mode is not False:
|
|
94
|
+
_reject("Pooling requires ceil_mode=False")
|
|
95
|
+
if isinstance(module, nn.MaxPool2d):
|
|
96
|
+
if _pair(module.dilation) != (1, 1) or module.return_indices is not False:
|
|
97
|
+
_reject("MaxPool2d requires dilation=1 and return_indices=False")
|
|
98
|
+
reducer, include = "max", None
|
|
99
|
+
elif module.divisor_override is not None or not isinstance(module.count_include_pad, bool):
|
|
100
|
+
_reject("AvgPool2d requires divisor_override=None and static count_include_pad")
|
|
101
|
+
else:
|
|
102
|
+
reducer, include = "mean", module.count_include_pad
|
|
103
|
+
kernel, strides, padding = _pair(module.kernel_size), _pair(stride), _pair(module.padding)
|
|
104
|
+
for k in ALIGNED_KERNELS:
|
|
105
|
+
if kernel == (k, k) and strides == (k, k) and padding == (0, 0):
|
|
106
|
+
return PoolConfig(reducer, k, k, 0, include)
|
|
107
|
+
if kernel == (3, 3) and padding == (1, 1):
|
|
108
|
+
if strides == (1, 1) or (strides == (2, 2) and reducer == "max"):
|
|
109
|
+
return PoolConfig(reducer, 3, int(strides[0]), 1, include)
|
|
110
|
+
_reject(f"Pooling supports {SUPPORTED}, without ceil_mode; received {type(module).__name__}"
|
|
111
|
+
f"(kernel_size={module.kernel_size!r}, stride={stride!r}, padding={module.padding!r})")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def pool_options(module: nn.AvgPool2d | nn.MaxPool2d) -> bool:
|
|
115
|
+
"""Validate the module and return whether it pools a padded 3x3 neighbourhood
|
|
116
|
+
(``pool_config(module).padding``), not an aligned k x k block."""
|
|
117
|
+
return not pool_config(module).aligned
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def pool2d(raster: Raster, module: nn.AvgPool2d | nn.MaxPool2d,
|
|
121
|
+
*, clip_footprints: bool = False, policy: str = FLOAT32_OPERANDS) -> Raster:
|
|
122
|
+
"""PyTorch average or max pooling at a supported configuration (``pool_config``)."""
|
|
123
|
+
validate_policy(policy, allowed=FLOAT32_OPERANDS)
|
|
124
|
+
config = pool_config(module)
|
|
125
|
+
if not config.aligned:
|
|
126
|
+
kernel = ee.Kernel.square(1, "pixels", False)
|
|
127
|
+
if config.reducer == "max":
|
|
128
|
+
# Masked padding contributes no value to max, just as -infinity.
|
|
129
|
+
result = raster.image.reduceNeighborhood(ee.Reducer.max(), kernel, skipMasked=False)
|
|
130
|
+
elif config.count_include_pad:
|
|
131
|
+
# count_include_pad=True: the in-bounds sum over the full window of 9
|
|
132
|
+
# (PyTorch's zero padding counts in the divisor).
|
|
133
|
+
result = raster.image.reduceNeighborhood(ee.Reducer.sum(), kernel, skipMasked=False).divide(9)
|
|
134
|
+
else:
|
|
135
|
+
# count_include_pad=False: the mean of the in-bounds pixels only.
|
|
136
|
+
result = raster.image.reduceNeighborhood(ee.Reducer.mean(), kernel, skipMasked=False)
|
|
137
|
+
if config.stride == 1:
|
|
138
|
+
# The output is on the input grid; _stride_sample(..., 1) is _finish,
|
|
139
|
+
# which pins it there (as conv2d does at stride 1).
|
|
140
|
+
return _stride_sample(result, raster.bands, raster.grid, 1, clip_footprints)
|
|
141
|
+
result = _project(result, raster.grid, clip_footprints)
|
|
142
|
+
return _stride_sample(result, raster.bands, raster.grid, config.stride, clip_footprints)
|
|
143
|
+
k = config.kernel
|
|
144
|
+
reducer = ee.Reducer.max() if config.reducer == "max" else ee.Reducer.mean()
|
|
145
|
+
if raster.grid.width % k or raster.grid.height % k:
|
|
146
|
+
raise UnsupportedOperation(aligned_size_message(k))
|
|
147
|
+
grid = raster.grid.resized(k, raster.grid.width // k, raster.grid.height // k)
|
|
148
|
+
# maxPixels (k + 2)^2 keeps today's 16 at k = 2: the k x k block plus a
|
|
149
|
+
# one-pixel ring. It bounds the input pixels per output pixel; with
|
|
150
|
+
# bestEffort=False a larger need fails the request rather than changing values.
|
|
151
|
+
result = raster.image.reduceResolution(reducer, bestEffort=False, maxPixels=(k + 2) ** 2)
|
|
152
|
+
result = _project(result, grid, clip_footprints)
|
|
153
|
+
return _finish(result, raster.bands, grid, clip_footprints)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
# Locality and cost metadata (contract section 9)
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
METADATA = validate_metadata({
|
|
160
|
+
# receptive_field._window_axis: output pixel i of an aligned k x k pool reads
|
|
161
|
+
# input pixels [k i, k i + k - 1], and a padded 3x3 pool with stride s reads
|
|
162
|
+
# [s i - 1, s i + 1]. The radius bounds the offset from s i: k - 1 = 3 at the
|
|
163
|
+
# largest aligned kernel, 4; 1 for the padded pools.
|
|
164
|
+
# Validated, in PoolConfig's terms (``pool_config(module)``; "mean" is AvgPool2d): the
|
|
165
|
+
# status's three families. count_include_pad does not change an unpadded pool, so the
|
|
166
|
+
# aligned pools do not name it.
|
|
167
|
+
"pool2d": BlockMetadata(
|
|
168
|
+
name="pool2d", layout_in="R", layout_out="R", locality=Locality.KERNEL,
|
|
169
|
+
policy=FLOAT32_OPERANDS, embedded_constants=_no_coefficients,
|
|
170
|
+
asset_arrays=_no_coefficients, radius=3,
|
|
171
|
+
status=("Ready — aligned avg/max k = 2, 3, 4; max 3×3 stride 1 or 2, padding 1; "
|
|
172
|
+
"avg 3×3 stride 1 padding 1, both divisor rules"),
|
|
173
|
+
evidence=("docs/evidence/live-fixtures/20261013T065257Z-p02.json", "docs/evidence/p02-raster-offline.json",
|
|
174
|
+
"docs/history/cnn/results/parity.json", "docs/evidence/t05-primary.json",
|
|
175
|
+
"docs/history/cnn/results/resnet_primitives.json", "docs/history/cnn/results/resnet18_full64_float32_policy.json",
|
|
176
|
+
"docs/history/cnn/results/segwater_js_parity_1x1.json"),
|
|
177
|
+
validated=Validated((
|
|
178
|
+
*({"reducer": ("mean", "max"), "kernel": (k,), "stride": (k,), "padding": (0,)} for k in ALIGNED_KERNELS),
|
|
179
|
+
{"reducer": ("max",), "kernel": (3,), "stride": (1, 2), "padding": (1,)},
|
|
180
|
+
{"reducer": ("mean",), "kernel": (3,), "stride": (1,), "padding": (1,), "count_include_pad": (True, False)},
|
|
181
|
+
), sources=(PoolConfig,))),
|
|
182
|
+
})
|