Skip to content

Commit e8403c8

Browse files
lint: tighten flake8 strict-select to all-style + sync pre-commit + dev-onboarding
Adds the full E-class style codes plus the pyflakes bug-detectors to the CI strict-select list so they fail the build instead of merely warning. Local pre-commit config now mirrors CI, so 'green locally' implies 'green CI'. Plus README onboarding for new contributors. Strict-select gates now include: * E9 / F63 / F7 / F82 syntax errors, undefined names * F401 / F811 / F841 unused imports / redefinitions / unused locals * E202 whitespace before '}' * E226 missing whitespace around arithmetic operator * E251 unexpected spaces around keyword/parameter equals * E402 module-level import not at top of file * E501 line too long (88-char limit, matches BoTorch) * E741 ambiguous variable name ('I'/'l'/'O') Files modified to conform: * boxcrete/slump_model.py drop unused 'import torch' * docs/generate_mix_analyses.py drop unused 'cost'/'str_1' locals; wrap ~20 long f-strings * test/test_partial_fixed_noise_likelihood.py drop unused 'n_total' * boxcrete/features.py move 'from boxcrete.utils import DEFAULT_X_COLUMNS' to top of module * experiments/regenerate_strength_json.py relocate '# noqa: E402' to opening line of multi-line import * boxcrete/plotting.py:191 rename 'I' -> 'eye_n' (E741) * test/test_models.py:412 rename 'I' -> 'eye_n' (E741) * boxcrete/utils.py:275-276,467 f-string '{x = }' -> '{x=}' (E202/E251) * experiments/check_artifacts_drift.py '*100' -> '* 100' (E226) * test/test_lengthscale_identifiability.py '*100' -> '* 100' (E226) * boxcrete/concrete_model.py wrap 5 long docstring/error lines * boxcrete/kernels.py wrap 196-char inline comment * boxcrete/likelihoods.py wrap 4 long pragma-comment lines * boxcrete/utils.py wrap 5 long docstring/error lines * boxcrete/slump_model.py wrap 1 long module-docstring line * docs/generate_mix_analyses.py wrap 1 long obs-str line * experiments/regenerate_strength_json.py wrap 2 long print lines * test/test_models.py:220 tighten long docstring * test/test_strength_curve_monotonicity.py wrap 3 long error-msg lines * test/test_utils.py:679 tighten long docstring Bonus fix in .flake8: explicit 'exclude' list was silently overriding flake8's defaults, so .git, __pycache__, .hg, etc. were being linted. Sapling stores backup copies of working-tree files under .git/sl/origbackups/ — without the explicit re-add of the default excludes, flake8 was reporting violations from old deleted research code. Defaults restored explicitly. Developer ergonomics: * .pre-commit-config.yaml synced to the CI strict-select so the local pre-commit hook fails on the same codes CI does (no more 'green locally, red on CI' surprises). * README adds a 'Development' section explaining 'pip install -e ".[dev]" && pre-commit install' onboarding, the role of black (auto-fix) vs flake8 (gate), and the 88-char line convention (matches BoTorch/GPyTorch).
1 parent f974a55 commit e8403c8

19 files changed

Lines changed: 231 additions & 106 deletions

.flake8

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,18 @@
22
# E203, W503: black and flake8 disagree on whitespace/operator placement
33
ignore = E203, W503
44
max-line-length = 88
5-
exclude = build, dist, .eggs
5+
# When ``exclude`` is set explicitly it OVERRIDES flake8's defaults
6+
# (.svn, CVS, .bzr, .hg, .git, __pycache__, .tox, .eggs, *.egg) — so we
7+
# spell them out here. ``.git`` is critical: Sapling stores backup
8+
# copies of working-tree files under ``.git/sl/origbackups/`` and
9+
# without this exclude flake8 would lint them.
10+
exclude =
11+
.git,
12+
.hg,
13+
.svn,
14+
__pycache__,
15+
.tox,
16+
.eggs,
17+
*.egg,
18+
build,
19+
dist,

.github/workflows/tests.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ jobs:
9393
9494
- name: Lint with flake8
9595
run: |
96-
# Stop build if there are Python syntax errors or undefined names
97-
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
98-
# Exit-zero treats all errors as warnings
96+
# Hard gate: syntax errors, undefined names, unused imports /
97+
# locals (real bugs), AND style violations (long lines,
98+
# whitespace, etc.) — keeping local pre-commit and CI in sync
99+
# so 'green locally' implies 'green on CI'.
100+
flake8 . --count --select=E9,E202,E226,E251,E402,E501,E741,F401,F63,F7,F811,F82,F841 --show-source --statistics
101+
# Exit-zero treats remaining E/W warnings as informational.
99102
flake8 . --count --exit-zero --statistics

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ repos:
2020
rev: 7.1.1
2121
hooks:
2222
- id: flake8
23-
# CI's hard gate: fail only on syntax errors / undefined names.
24-
# Style warnings are reported but non-blocking on CI, and we keep
25-
# the same posture locally to avoid pre-commit becoming a nag.
23+
# Mirrors the CI lint gate in .github/workflows/tests.yml. Fails
24+
# on the same codes CI fails on, so a successful local commit
25+
# implies a green lint job upstream.
2626
args:
2727
- --count
28-
- --select=E9,F63,F7,F82
28+
- --select=E9,E202,E226,E251,E402,E501,E741,F401,F63,F7,F811,F82,F841
2929
- --show-source
3030
- --statistics

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,30 @@ For the earlier workshop paper that introduced the model with mortar data, pleas
235235
}
236236
```
237237

238+
## Development
239+
240+
For local development, install with the `dev` extras and register the
241+
pre-commit hook so every `git commit` is auto-checked against the same
242+
lint gate the CI uses:
243+
244+
```bash
245+
pip install -e ".[dev]"
246+
pre-commit install
247+
```
248+
249+
`black` will auto-format whitespace, quoting, and most line wrapping on
250+
commit. `flake8` will fail the commit if it finds any of: syntax errors,
251+
undefined names, unused imports / locals, late imports, ambiguous names,
252+
or lines over 88 characters. To run the gate manually across the whole
253+
repo (matching CI):
254+
255+
```bash
256+
pre-commit run --all-files
257+
```
258+
259+
When adding a new long string literal or comment that black can't auto-
260+
split, hand-wrap it to ≤ 88 columns — the same convention BoTorch and
261+
GPyTorch use.
262+
238263
## License
239264
`SustainableConcrete` is released under the MIT license, as found in the LICENSE file.

boxcrete/concrete_model.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ def __init__(
6565
slump_model: The slump model. Defaults to None.
6666
cost_model: The cost model. Defaults to None.
6767
d: The dimensionality of the input to the strength model.
68-
Is inferred automatically if the fit functions are called. NOTE: The model
69-
assumes that the last element of the input corresponds to the time dimension.
68+
Is inferred automatically if the fit functions are called.
69+
NOTE: The model assumes that the last element of the input
70+
corresponds to the time dimension.
7071
"""
7172
self.strength_days = strength_days
7273
self.strength_model = strength_model
@@ -273,7 +274,8 @@ def get_model_list(
273274
"""
274275
if self.d is None or self.strength_model is None or self.gwp_model is None:
275276
raise ValueError(
276-
"Model not fit yet. Call fit_gwp_model() and fit_strength_model() first."
277+
"Model not fit yet. Call fit_gwp_model() and "
278+
"fit_strength_model() first."
277279
)
278280

279281
time_idx = self.d - 1 # last column is Time
@@ -330,7 +332,8 @@ def model_names(self) -> list[str]:
330332
"""Ordered names of outputs in the ``ModelList`` from ``get_model_list``.
331333
332334
Returns:
333-
A list like ``["GWP", "1-day Strength", "28-day Strength", "Slump (in)", "Cost"]``.
335+
A list like
336+
``["GWP", "1-day Strength", "28-day Strength", "Slump (in)", "Cost"]``.
334337
"""
335338
names = ["GWP"]
336339
for day in self.strength_days:
@@ -363,7 +366,8 @@ def get_model_dict(
363366
) -> dict[str, Model]:
364367
"""Returns a name-to-model dictionary for the multi-output model.
365368
366-
Equivalent to ``dict(zip(model.model_names, model.get_model_list(...).models))``.
369+
Equivalent to
370+
``dict(zip(model.model_names, model.get_model_list(...).models))``.
367371
368372
Args:
369373
fixed_features: Same as ``get_model_list``.

boxcrete/features.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from botorch.models.transforms.input import InputTransform
3636
from torch import Tensor
3737

38+
from boxcrete.utils import DEFAULT_X_COLUMNS
39+
3840
# Time gate constant: h(t) = 1 - exp(-t / GATE_TAU).
3941
# tau=0.05 (post-input-transform time units) was found to be optimal in
4042
# the τ-sweep; see §4.5 of the benchmark.
@@ -54,8 +56,6 @@
5456
)
5557

5658

57-
from boxcrete.utils import DEFAULT_X_COLUMNS
58-
5959
# Short alias → full column name in :data:`boxcrete.utils.DEFAULT_X_COLUMNS`.
6060
# The integer indices in :data:`IDX` are *derived* from
6161
# ``DEFAULT_X_COLUMNS`` at import time, so any reorder / addition /
@@ -176,10 +176,11 @@ def append_engineered_features_callable(
176176
"""
177177

178178
def f(X: torch.Tensor) -> torch.Tensor:
179-
if (
180-
not feature_names
181-
): # pragma: no cover -- V2 fit always has F5_alllog (7 features); empty-feature_names branch only used by research-only ablations
182-
return X.new_empty((*X.shape[:-1], 1, 0))
179+
if not feature_names:
180+
# ``pragma: no cover`` -- V2 fit always has F5_alllog
181+
# (7 features); empty-feature_names branch only used by
182+
# research-only ablations.
183+
return X.new_empty((*X.shape[:-1], 1, 0)) # pragma: no cover
183184
feats = torch.cat([FEATURE_BUILDERS[n](X) for n in feature_names], dim=-1)
184185
return feats.unsqueeze(-2)
185186

@@ -197,10 +198,11 @@ def max_scale_Y(Y: Tensor) -> tuple[Tensor, Tensor, Tensor]:
197198
Returns ``(Y_scaled, y_mean=0, y_std=y_max)`` so the existing untransform
198199
code path ``mean * y_std + y_mean`` works correctly.
199200
"""
200-
if (
201-
Y.dim() == 1
202-
): # pragma: no cover -- V2 callers pass [n, 1] Y (Y-shape guard at fit_strength_gp top); 1D fallback retained for symmetry with BoTorch's Standardize signature
203-
Y = Y.unsqueeze(-1)
201+
if Y.dim() == 1:
202+
# ``pragma: no cover`` -- V2 callers pass [n, 1] Y (Y-shape
203+
# guard at ``fit_strength_gp`` top); 1D fallback retained for
204+
# symmetry with BoTorch's Standardize signature.
205+
Y = Y.unsqueeze(-1) # pragma: no cover
204206
y_max = Y.abs().max(dim=0, keepdim=True).values.clamp_min(1e-6)
205207
y_mean = torch.zeros_like(y_max)
206208
return Y / y_max, y_mean, y_max
@@ -215,10 +217,11 @@ def augmented_bounds(
215217
empirical [min, max] (with 5% padding) over the appended features
216218
evaluated on X.
217219
"""
218-
if (
219-
not feature_names
220-
): # pragma: no cover -- V2 fit always passes F5_alllog (7 features); empty-feature_names branch only used by research-only no-feature ablations
221-
return bounds
220+
if not feature_names:
221+
# ``pragma: no cover`` -- V2 fit always passes F5_alllog
222+
# (7 features); empty-feature_names branch only used by
223+
# research-only no-feature ablations.
224+
return bounds # pragma: no cover
222225
appended_vals = torch.cat([FEATURE_BUILDERS[n](X) for n in feature_names], dim=-1)
223226
aug_lower = appended_vals.min(dim=0).values
224227
aug_upper = appended_vals.max(dim=0).values

boxcrete/kernels.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,11 @@ def forward(self, x1, x2, diag=False, last_dim_is_batch=False, **params):
173173
h2 = self._h(x2[..., self.time_idx])
174174
if diag:
175175
# K shape: [..., n] — element-wise multiply by h1, h2 (same shape)
176-
return (
177-
K * h1 * h2
178-
) # pragma: no cover -- diag=True kernel-eval branch; BoTorch's posterior(...).variance computes full covariance and extracts the diagonal, never calling kernel.forward with diag=True
176+
# ``diag=True`` kernel-eval branch; BoTorch's
177+
# ``posterior(...).variance`` computes the full covariance
178+
# and extracts the diagonal, so kernel.forward is never
179+
# called with diag=True in the production fit path.
180+
return K * h1 * h2 # pragma: no cover
179181
# K shape: [..., n1, n2]; multiply by h1[...,n1,1] and h2[...,1,n2]
180182
return K * h1.unsqueeze(-1) * h2.unsqueeze(-2)
181183

boxcrete/likelihoods.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,12 @@ def __init__(
128128
noise_prior=None,
129129
**kwargs,
130130
):
131-
if (
132-
noise_constraint is None
133-
): # pragma: no cover -- production callers always pass an explicit noise_constraint via build_strength_kernel_for_aug_dim; this default-fallback path is research-only
134-
noise_constraint = LogTransformedInterval(
131+
if noise_constraint is None:
132+
# ``pragma: no cover`` -- production callers always pass
133+
# an explicit ``noise_constraint`` via
134+
# ``build_strength_kernel_for_aug_dim``; this default-
135+
# fallback path is research-only.
136+
noise_constraint = LogTransformedInterval( # pragma: no cover
135137
1e-6,
136138
1.0,
137139
initial_value=1e-1,
@@ -161,9 +163,10 @@ def noise(self) -> torch.Tensor:
161163
doesn't proxy this automatically when we override
162164
``_shaped_noise_covar`` with a ``HomoskedasticNoise`` placeholder,
163165
so we expose it explicitly here."""
164-
return (
165-
self.noise_covar.noise
166-
) # pragma: no cover -- exposed for notebook ergonomics; production fit/predict paths read self.noise_covar.noise directly
166+
# ``pragma: no cover`` -- exposed for notebook ergonomics;
167+
# production fit/predict paths read ``self.noise_covar.noise``
168+
# directly.
169+
return self.noise_covar.noise # pragma: no cover
167170

168171
def set_train_times(self, time_values: torch.Tensor) -> None:
169172
"""Stash post-input-transform train times so the MLL path (which
@@ -193,8 +196,13 @@ def _shaped_noise_covar(self, base_shape, *params, **kwargs):
193196
t = params[0][..., self.time_idx]
194197
elif getattr(self, "_train_times", None) is not None:
195198
t = self._train_times
196-
else: # pragma: no cover -- defensive fallback; V2 fit always either passes a 2D X (training) or has _train_times set (eval) before this method is called
197-
return super()._shaped_noise_covar(base_shape, *params, **kwargs)
199+
else:
200+
# ``pragma: no cover`` -- defensive fallback; V2 fit always
201+
# either passes a 2D X (training) or has ``_train_times``
202+
# set (eval) before this method is called.
203+
return super()._shaped_noise_covar( # pragma: no cover
204+
base_shape, *params, **kwargs
205+
)
198206
h = self._gate(t)
199207
h2 = h * h # element-wise [n]
200208
# Scalar global noise (broadcasted to per-row).
@@ -203,9 +211,12 @@ def _shaped_noise_covar(self, base_shape, *params, **kwargs):
203211
n = int(base_shape[-1])
204212
if per_row_var.shape[0] >= n:
205213
per_row_var = per_row_var[:n]
206-
else: # pragma: no cover -- pad branch; V2 fit pre-sizes _train_times to match training data, so per_row_var.shape[0] always >= n
207-
pad = sigma2.expand(n - per_row_var.shape[0])
208-
per_row_var = torch.cat([per_row_var, pad], dim=0)
214+
else:
215+
# ``pragma: no cover`` -- pad branch; V2 fit pre-sizes
216+
# ``_train_times`` to match training data, so
217+
# ``per_row_var.shape[0]`` always >= n.
218+
pad = sigma2.expand(n - per_row_var.shape[0]) # pragma: no cover
219+
per_row_var = torch.cat([per_row_var, pad], dim=0) # pragma: no cover
209220
return DiagLinearOperator(per_row_var)
210221

211222

boxcrete/plotting.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ def compute_loo_cv(
188188
L = psd_safe_cholesky(K)
189189
residuals = (train_Y - prior_dist.mean).unsqueeze(-1)
190190
K_inv_res = torch.cholesky_solve(residuals, L)
191-
I = torch.eye(n, dtype=L.dtype, device=L.device)
192-
L_inv = torch.linalg.solve_triangular(L, I, upper=False)
191+
eye_n = torch.eye(n, dtype=L.dtype, device=L.device)
192+
L_inv = torch.linalg.solve_triangular(L, eye_n, upper=False)
193193
K_inv_diag = (L_inv**2).sum(dim=-2)
194194
loo_var = (1.0 / K_inv_diag).unsqueeze(-1)
195195
loo_mean = train_Y.unsqueeze(-1) - K_inv_res * loo_var

boxcrete/slump_model.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
* :func:`fit_slump_gp` — fit a slump GP on raw (X, Y, Yvar) data.
1818
1919
The HRWR/binder ratio is appended via :class:`AppendDerivedFeatures`
20-
from :mod:`boxcrete.features` (the same transform :mod:`boxcrete.strength_model_legacy`'s
21-
V1 input transform composes with).
20+
from :mod:`boxcrete.features` (the same transform
21+
:mod:`boxcrete.strength_model_legacy`'s V1 input transform composes with).
2222
"""
2323

2424
from __future__ import annotations
2525

26-
import torch
2726
from botorch import fit_gpytorch_mll
2827
from botorch.models import SingleTaskGP
2928
from botorch.models.transforms.input import ChainedInputTransform, Normalize

0 commit comments

Comments
 (0)