optixstuff 1.5.0__tar.gz → 2.0.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.
- {optixstuff-1.5.0 → optixstuff-2.0.0}/.pre-commit-config.yaml +8 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/CHANGELOG.md +18 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/PKG-INFO +5 -3
- {optixstuff-1.5.0 → optixstuff-2.0.0}/README.md +4 -2
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/__init__.py +2 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/_version.py +2 -2
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/disperser.py +14 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/optical_path.py +11 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/primary.py +28 -5
- optixstuff-2.0.0/src/optixstuff/speckle.py +85 -0
- optixstuff-2.0.0/tools/check_internal_refs.py +80 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/.gitignore +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/.readthedocs.yaml +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/.zenodo.json +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/CITATION.cff +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/LICENSE +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/pyproject.toml +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/_repr.py +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/coronagraph.py +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/detector.py +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/exposure.py +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/optical_elements.py +0 -0
- {optixstuff-1.5.0 → optixstuff-2.0.0}/src/optixstuff/yippy_coronagraph.py +0 -0
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
repos:
|
|
2
|
+
- repo: local
|
|
3
|
+
hooks:
|
|
4
|
+
- id: no-internal-refs
|
|
5
|
+
name: no internal research references
|
|
6
|
+
entry: python3 tools/check_internal_refs.py
|
|
7
|
+
language: system
|
|
8
|
+
types: [text]
|
|
9
|
+
exclude: ^(CHANGELOG\.md|tools/check_internal_refs\.py)$
|
|
2
10
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
11
|
rev: "v6.0.0"
|
|
4
12
|
hooks:
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.0.0](https://github.com/CoreySpohn/optixstuff/compare/v1.5.0...v2.0.0) (2026-07-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **disperser:** sky_pitch_arcsec replaces sky_pitch_lod -- the cube grid is fixed-angle, so the pitch is a plain angle
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **disperser:** sky_pitch_lod + psflet_pack_path descriptor fields ([d2ae639](https://github.com/CoreySpohn/optixstuff/commit/d2ae6392a5f24a2c3b43413149dd7e89b7125600))
|
|
13
|
+
* **primary:** exact segment geometry -- optional segment_point_to_point_m and inscribed_diameter_m, segment_pitch_m, flat-top segment_centres_m matching the HWO EAC baseline pupils ([4c5b4b7](https://github.com/CoreySpohn/optixstuff/commit/4c5b4b71843450339c2a330d74833a5b53a5431d))
|
|
14
|
+
* **speckle:** add AbstractSpeckleField interface and OpticalPath.speckle ([fb7b501](https://github.com/CoreySpohn/optixstuff/commit/fb7b501a2b105350786f757c7c5c761f8bfef2b0))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Code Refactoring
|
|
18
|
+
|
|
19
|
+
* **disperser:** sky_pitch_arcsec replaces sky_pitch_lod -- the cube grid is fixed-angle, so the pitch is a plain angle ([31041e2](https://github.com/CoreySpohn/optixstuff/commit/31041e2b85402b083ce2e7913100cfc2919452f3))
|
|
20
|
+
|
|
3
21
|
## [1.5.0](https://github.com/CoreySpohn/optixstuff/compare/v1.4.0...v1.5.0) (2026-06-27)
|
|
4
22
|
|
|
5
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optixstuff
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Hardware abstractions for the HWO direct imaging simulation suite
|
|
5
5
|
Project-URL: Homepage, https://github.com/CoreySpohn/optixstuff
|
|
6
6
|
Project-URL: Issues, https://github.com/CoreySpohn/optixstuff/issues
|
|
@@ -94,8 +94,10 @@ Built on [JAX](https://github.com/google/jax) and
|
|
|
94
94
|
|
|
95
95
|
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
96
96
|
`AbstractCoronagraph`, `AbstractDetector`, `AbstractDisperser`
|
|
97
|
-
- **Concrete implementations** — `SimplePrimary`, `
|
|
98
|
-
|
|
97
|
+
- **Concrete implementations** — `SimplePrimary`, `SegmentedPrimary` (carries
|
|
98
|
+
hex segment geometry for a diffraction backend), `ConstantThroughput`,
|
|
99
|
+
`IdealDetector`, `Detector` (with optional wavelength-dependent QE),
|
|
100
|
+
`LensletDisperser`
|
|
99
101
|
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
100
102
|
simulators
|
|
101
103
|
|
|
@@ -36,8 +36,10 @@ Built on [JAX](https://github.com/google/jax) and
|
|
|
36
36
|
|
|
37
37
|
- **Abstract interfaces** — `AbstractPrimary`, `AbstractOpticalElement`,
|
|
38
38
|
`AbstractCoronagraph`, `AbstractDetector`, `AbstractDisperser`
|
|
39
|
-
- **Concrete implementations** — `SimplePrimary`, `
|
|
40
|
-
|
|
39
|
+
- **Concrete implementations** — `SimplePrimary`, `SegmentedPrimary` (carries
|
|
40
|
+
hex segment geometry for a diffraction backend), `ConstantThroughput`,
|
|
41
|
+
`IdealDetector`, `Detector` (with optional wavelength-dependent QE),
|
|
42
|
+
`LensletDisperser`
|
|
41
43
|
- **Container** — `OpticalPath`, a composable hardware configuration passed to all
|
|
42
44
|
simulators
|
|
43
45
|
|
|
@@ -20,6 +20,7 @@ from optixstuff.optical_elements import (
|
|
|
20
20
|
)
|
|
21
21
|
from optixstuff.optical_path import OpticalPath
|
|
22
22
|
from optixstuff.primary import AbstractPrimary, SegmentedPrimary, SimplePrimary
|
|
23
|
+
from optixstuff.speckle import AbstractSpeckleField
|
|
23
24
|
from optixstuff.yippy_coronagraph import YippyCoronagraph
|
|
24
25
|
|
|
25
26
|
__all__ = [
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
"AbstractOpticalElement",
|
|
30
31
|
"AbstractPrimary",
|
|
31
32
|
"AbstractScalarCoronagraph",
|
|
33
|
+
"AbstractSpeckleField",
|
|
32
34
|
"AbstractUniformElement",
|
|
33
35
|
"ConstantThroughput",
|
|
34
36
|
"Detector",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '2.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 0, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -63,6 +63,18 @@ class LensletDisperser(AbstractDisperser):
|
|
|
63
63
|
``lambda f / D``, so at fixed pixel scale coronachrome scales the core width
|
|
64
64
|
by ``lambda / psflet_ref_nm`` per wavelength (the shape parameters do not
|
|
65
65
|
scale).
|
|
66
|
+
|
|
67
|
+
``sky_pitch_arcsec`` is the lenslet pitch projected on sky, a plain angle
|
|
68
|
+
in arcseconds (the focal-plane cube a render layer consumes lives on a
|
|
69
|
+
fixed angular grid, so no reference wavelength is involved). It makes the
|
|
70
|
+
spatial sampling an instrument property: the render layer derives
|
|
71
|
+
focal-plane pixels per lenslet as the ratio of this pitch to the cube's
|
|
72
|
+
angular plate scale instead of taking a free knob. ``None`` means
|
|
73
|
+
unspecified.
|
|
74
|
+
|
|
75
|
+
``psflet_pack_path`` is a reference to a frozen PSFlet template pack file
|
|
76
|
+
(used with ``psflet_kind="template"``), the way ``throughput_element``
|
|
77
|
+
carries the blaze curve: config only, the render layer loads it.
|
|
66
78
|
"""
|
|
67
79
|
|
|
68
80
|
pitch_m: float
|
|
@@ -78,6 +90,8 @@ class LensletDisperser(AbstractDisperser):
|
|
|
78
90
|
psflet_kind: str = eqx.field(static=True)
|
|
79
91
|
detector_shape: tuple[int, int] = eqx.field(static=True)
|
|
80
92
|
throughput_element: AbstractOpticalElement = ConstantThroughput(1.0)
|
|
93
|
+
sky_pitch_arcsec: float | None = None
|
|
94
|
+
psflet_pack_path: str | None = eqx.field(static=True, default=None)
|
|
81
95
|
|
|
82
96
|
def _dispersion_px(self, wavelength_nm):
|
|
83
97
|
"""Spectral-axis detector offset [px] for the wavelength(s)."""
|
|
@@ -16,6 +16,7 @@ from optixstuff.optical_elements import (
|
|
|
16
16
|
ConstantThroughput,
|
|
17
17
|
)
|
|
18
18
|
from optixstuff.primary import AbstractPrimary, SimplePrimary
|
|
19
|
+
from optixstuff.speckle import AbstractSpeckleField
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class OpticalPath(eqx.Module):
|
|
@@ -38,6 +39,9 @@ class OpticalPath(eqx.Module):
|
|
|
38
39
|
not a spectral channel count). Default 1.0.
|
|
39
40
|
npix_multiplier: IFS signal-spread multiplier on detector pixel
|
|
40
41
|
counts. Default 1.0.
|
|
42
|
+
speckle: Optional time-varying speckle field (the stochastic
|
|
43
|
+
wavefront-error residual on top of the deterministic
|
|
44
|
+
coronagraph leakage floor); None for a perfect-wavefront path.
|
|
41
45
|
"""
|
|
42
46
|
|
|
43
47
|
primary: AbstractPrimary
|
|
@@ -47,6 +51,7 @@ class OpticalPath(eqx.Module):
|
|
|
47
51
|
disperser: AbstractDisperser | None = None
|
|
48
52
|
n_channels: float = 1.0
|
|
49
53
|
npix_multiplier: float = 1.0
|
|
54
|
+
speckle: AbstractSpeckleField | None = None
|
|
50
55
|
|
|
51
56
|
@classmethod
|
|
52
57
|
def from_default_setup(
|
|
@@ -62,6 +67,7 @@ class OpticalPath(eqx.Module):
|
|
|
62
67
|
dark_current_rate_e_per_s: float = 0.0,
|
|
63
68
|
n_channels: float = 1.0,
|
|
64
69
|
npix_multiplier: float = 1.0,
|
|
70
|
+
speckle: AbstractSpeckleField | None = None,
|
|
65
71
|
) -> OpticalPath:
|
|
66
72
|
"""Build an OpticalPath with reasonable HWO-like defaults.
|
|
67
73
|
|
|
@@ -92,6 +98,8 @@ class OpticalPath(eqx.Module):
|
|
|
92
98
|
callers add realistic noise when needed).
|
|
93
99
|
n_channels: AYO parallel-path multiplier. Default ``1.0``.
|
|
94
100
|
npix_multiplier: IFS signal-spread multiplier. Default ``1.0``.
|
|
101
|
+
speckle: Optional speckle field attached to the path. Default
|
|
102
|
+
``None`` -- a perfect-wavefront path.
|
|
95
103
|
|
|
96
104
|
Returns:
|
|
97
105
|
A ready-to-use :class:`OpticalPath`.
|
|
@@ -124,6 +132,7 @@ class OpticalPath(eqx.Module):
|
|
|
124
132
|
),
|
|
125
133
|
n_channels=n_channels,
|
|
126
134
|
npix_multiplier=npix_multiplier,
|
|
135
|
+
speckle=speckle,
|
|
127
136
|
)
|
|
128
137
|
|
|
129
138
|
def system_throughput(self, wavelength_nm: float) -> float:
|
|
@@ -158,4 +167,6 @@ class OpticalPath(eqx.Module):
|
|
|
158
167
|
lines.append(" attenuating_elements: ()")
|
|
159
168
|
lines.append(indent("coronagraph: " + repr(self.coronagraph)))
|
|
160
169
|
lines.append(indent("detector: " + repr(self.detector)))
|
|
170
|
+
if self.speckle is not None:
|
|
171
|
+
lines.append(indent("speckle: " + repr(self.speckle)))
|
|
161
172
|
return "\n".join(lines)
|
|
@@ -100,6 +100,8 @@ class SegmentedPrimary(AbstractPrimary):
|
|
|
100
100
|
_diameter_m: float
|
|
101
101
|
_area_m2: float
|
|
102
102
|
segment_gap_m: float
|
|
103
|
+
segment_point_to_point_m: float | None
|
|
104
|
+
inscribed_diameter_m: float | None
|
|
103
105
|
n_rings: int = eqx.field(static=True)
|
|
104
106
|
n_segments: int = eqx.field(static=True)
|
|
105
107
|
segment_shape: str = eqx.field(static=True)
|
|
@@ -112,11 +114,15 @@ class SegmentedPrimary(AbstractPrimary):
|
|
|
112
114
|
n_segments: int,
|
|
113
115
|
segment_gap_m: float,
|
|
114
116
|
segment_shape: str = "hexagon",
|
|
117
|
+
segment_point_to_point_m: float | None = None,
|
|
118
|
+
inscribed_diameter_m: float | None = None,
|
|
115
119
|
) -> None:
|
|
116
120
|
"""Create a segmented hex primary from its geometry."""
|
|
117
121
|
self._diameter_m = diameter_m
|
|
118
122
|
self._area_m2 = area_m2
|
|
119
123
|
self.segment_gap_m = segment_gap_m
|
|
124
|
+
self.segment_point_to_point_m = segment_point_to_point_m
|
|
125
|
+
self.inscribed_diameter_m = inscribed_diameter_m
|
|
120
126
|
self.n_rings = n_rings
|
|
121
127
|
self.n_segments = n_segments
|
|
122
128
|
self.segment_shape = segment_shape
|
|
@@ -133,21 +139,38 @@ class SegmentedPrimary(AbstractPrimary):
|
|
|
133
139
|
|
|
134
140
|
@property
|
|
135
141
|
def segment_flat_to_flat_m(self) -> float:
|
|
136
|
-
"""Segment flat-to-flat size in metres
|
|
142
|
+
"""Segment flat-to-flat size in metres.
|
|
143
|
+
|
|
144
|
+
Exact (``point_to_point * sqrt(3) / 2``) when the segment size is
|
|
145
|
+
given; otherwise the legacy circumscribing approximation
|
|
146
|
+
``diameter / (2 n_rings + 1)``.
|
|
147
|
+
"""
|
|
148
|
+
if self.segment_point_to_point_m is not None:
|
|
149
|
+
return self.segment_point_to_point_m * jnp.sqrt(3.0) / 2.0
|
|
137
150
|
return self._diameter_m / (2 * self.n_rings + 1)
|
|
138
151
|
|
|
152
|
+
@property
|
|
153
|
+
def segment_pitch_m(self) -> float:
|
|
154
|
+
"""Centre-to-centre distance of adjacent segments (flat-to-flat + gap)."""
|
|
155
|
+
return self.segment_flat_to_flat_m + self.segment_gap_m
|
|
156
|
+
|
|
139
157
|
@property
|
|
140
158
|
def segment_centres_m(self):
|
|
141
|
-
"""``(n_segments, 2)``
|
|
159
|
+
"""``(n_segments, 2)`` segment centre (x, y) positions in metres.
|
|
160
|
+
|
|
161
|
+
A flat-top hexagonal lattice (flats up, points along x), the
|
|
162
|
+
convention of the HWO EAC baseline pupils: the centre segment is
|
|
163
|
+
first, and adjacent centres are one ``segment_pitch_m`` apart.
|
|
164
|
+
"""
|
|
142
165
|
if self.segment_shape != "hexagon":
|
|
143
166
|
msg = f"segment_centres_m undefined for shape {self.segment_shape!r}"
|
|
144
167
|
raise NotImplementedError(msg)
|
|
145
168
|
axial = _hex_axial_coords(self.n_rings)
|
|
146
|
-
|
|
169
|
+
pitch = self.segment_pitch_m
|
|
147
170
|
qs = jnp.array([q for q, _ in axial])
|
|
148
171
|
rs = jnp.array([r for _, r in axial])
|
|
149
|
-
xs =
|
|
150
|
-
ys =
|
|
172
|
+
xs = pitch * (jnp.sqrt(3.0) / 2.0) * (qs + rs)
|
|
173
|
+
ys = pitch * (rs - qs) / 2.0
|
|
151
174
|
return jnp.stack([xs, ys], axis=-1)
|
|
152
175
|
|
|
153
176
|
def __repr__(self) -> str:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Speckle field abstractions.
|
|
2
|
+
|
|
3
|
+
A speckle field is the stochastic, time-varying residual-starlight pattern
|
|
4
|
+
left by wavefront errors that wavefront control does not null. It sits *on
|
|
5
|
+
top of* the deterministic coronagraphic leakage floor (the YIP
|
|
6
|
+
``stellar_intens`` map already applied in ``coronagraphoto.star_rate``), so
|
|
7
|
+
it is an instrument effect, not an astrophysical scene source -- it lives on
|
|
8
|
+
:class:`optixstuff.OpticalPath`, next to the coronagraph, rather than in the
|
|
9
|
+
skyscapes scene.
|
|
10
|
+
|
|
11
|
+
The standard linear speckle model is ``I(t) = |E_nom + G eps(t)|^2``, where
|
|
12
|
+
``E_nom`` is the static coherent residual field (``|E_nom|^2`` is the
|
|
13
|
+
deterministic floor), ``G`` maps wavefront-error modes to the focal-plane
|
|
14
|
+
field, and ``eps(t)`` are the drifting mode coefficients. Concrete fields
|
|
15
|
+
differ only in how they source ``G`` and ``eps`` (analytic, replayed
|
|
16
|
+
intensity cubes, fitted reduced-order, or learned); they share the
|
|
17
|
+
:class:`AbstractSpeckleField` contract below.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import abc
|
|
21
|
+
|
|
22
|
+
import equinox as eqx
|
|
23
|
+
from equinox import AbstractVar
|
|
24
|
+
from jax.typing import ArrayLike
|
|
25
|
+
from jaxtyping import Array
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AbstractSpeckleField(eqx.Module):
|
|
29
|
+
"""Abstract interface for time-varying speckle fields.
|
|
30
|
+
|
|
31
|
+
Implementations can be backed by an analytic generator (physicaloptix),
|
|
32
|
+
replayed designer/testbed intensity cubes, a fitted reduced-order model,
|
|
33
|
+
or a learned generator. All produce a coronagraph-plane contrast map
|
|
34
|
+
through :meth:`realize`.
|
|
35
|
+
|
|
36
|
+
All wavelength arguments are in nanometres; ``time_s`` is seconds since
|
|
37
|
+
mission start. The returned map is on the field's native coronagraph
|
|
38
|
+
plane at :attr:`pixel_scale_lod`; the caller resamples it to the
|
|
39
|
+
detector grid (as for the coronagraph ``stellar_intens`` map).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
pixel_scale_lod: AbstractVar[float]
|
|
43
|
+
"""Native pixel scale in lambda/D per pixel."""
|
|
44
|
+
|
|
45
|
+
epoch_jd: AbstractVar[float]
|
|
46
|
+
"""Julian Date that maps to ``time_s = 0`` -- the realization's clock
|
|
47
|
+
origin. Consumers (e.g. ``coronagraphoto.speckle_rate``) convert an
|
|
48
|
+
observation's absolute JD to the elapsed seconds :meth:`realize` expects
|
|
49
|
+
via ``(start_time_jd - epoch_jd)``; anchoring the clock here keeps the
|
|
50
|
+
physical interface in elapsed seconds and avoids feeding large absolute
|
|
51
|
+
JDs into the temporal synthesis."""
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
def realize(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
wavelength_nm: ArrayLike,
|
|
58
|
+
time_s: ArrayLike = 0.0,
|
|
59
|
+
) -> Array:
|
|
60
|
+
"""Speckle contrast *delta* at a given time and wavelength.
|
|
61
|
+
|
|
62
|
+
Returns the wavefront-error-induced excess over the deterministic
|
|
63
|
+
coronagraphic floor -- i.e. ``I(t) - |E_nom|^2``, expanded as
|
|
64
|
+
``2 Re(E_nom* . G eps(t)) + |G eps(t)|^2``, in contrast units
|
|
65
|
+
(fraction of the host-star flux per pixel). It must not include the
|
|
66
|
+
``|E_nom|^2`` floor itself: ``star_rate`` already applies that via
|
|
67
|
+
the YIP ``stellar_intens`` map, so re-emitting it here would double
|
|
68
|
+
count. An incoherent-halo implementation returns only the strictly
|
|
69
|
+
positive ``|G eps(t)|^2`` term (no speckle pinning); a coherent
|
|
70
|
+
implementation adds the cross term, which carries the bright-tail
|
|
71
|
+
pinning and requires the complex ``E_nom``.
|
|
72
|
+
|
|
73
|
+
Evolution is driven by ``time_s``, not a per-call PRNG key: the
|
|
74
|
+
realization is fixed at construction so the map is deterministic and
|
|
75
|
+
differentiable, and temporal correlation survives across an exposure
|
|
76
|
+
/ roll sequence.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
wavelength_nm: Wavelength in nanometres.
|
|
80
|
+
time_s: Time since mission start in seconds.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
2D float contrast-delta array on the native coronagraph plane.
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Commit guard: block internal research references in library files.
|
|
3
|
+
|
|
4
|
+
Library code is public and self-contained; pointers into the private research
|
|
5
|
+
workspace (project brain documents, decision-history vocabulary, working-note
|
|
6
|
+
paths) do not belong in it. This script scans the given files line by line
|
|
7
|
+
against a small pattern list and exits nonzero on any hit, so it can run as a
|
|
8
|
+
pre-commit hook (see .pre-commit-config.yaml) and as a pytest.
|
|
9
|
+
|
|
10
|
+
A line that must legitimately contain such a string (e.g. a development-only
|
|
11
|
+
data fallback path in test fixtures) can be waived by appending the comment
|
|
12
|
+
``internal-ref-ok`` to that line.
|
|
13
|
+
|
|
14
|
+
Usage: check_internal_refs.py FILE [FILE ...]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
WAIVER = "internal-ref-ok"
|
|
22
|
+
|
|
23
|
+
PATTERNS = [
|
|
24
|
+
# Research-workspace paths and project homes.
|
|
25
|
+
r"mission-control",
|
|
26
|
+
r"research-brain",
|
|
27
|
+
r"physicaloptix-setup",
|
|
28
|
+
r"\bbrain/",
|
|
29
|
+
r"deep-thoughts",
|
|
30
|
+
# Underscored ALL-CAPS markdown names are internal working documents.
|
|
31
|
+
r"\b[A-Z0-9]+(?:_[A-Z0-9]+)+\.md\b",
|
|
32
|
+
# Project spike/script homes and decision-history vocabulary.
|
|
33
|
+
r"eac1_dlux",
|
|
34
|
+
r"Tier-G",
|
|
35
|
+
r"(?i)\bgreenfield\b",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
COMPILED = [(pattern, re.compile(pattern)) for pattern in PATTERNS]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def scan_file(path):
|
|
42
|
+
"""Return (lineno, pattern, line) hits for one file."""
|
|
43
|
+
hits = []
|
|
44
|
+
try:
|
|
45
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
46
|
+
except OSError as err:
|
|
47
|
+
print(f"{path}: unreadable ({err})")
|
|
48
|
+
return [(0, "<unreadable>", "")]
|
|
49
|
+
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
50
|
+
if WAIVER in line:
|
|
51
|
+
continue
|
|
52
|
+
for pattern, regex in COMPILED:
|
|
53
|
+
if regex.search(line):
|
|
54
|
+
hits.append((lineno, pattern, line.strip()))
|
|
55
|
+
return hits
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def main(argv):
|
|
59
|
+
"""Scan every argument path; report hits; exit 1 on any."""
|
|
60
|
+
self_path = Path(__file__).resolve()
|
|
61
|
+
failed = False
|
|
62
|
+
for arg in argv:
|
|
63
|
+
path = Path(arg)
|
|
64
|
+
if path.resolve() == self_path:
|
|
65
|
+
continue
|
|
66
|
+
for lineno, pattern, line in scan_file(path):
|
|
67
|
+
failed = True
|
|
68
|
+
print(f"{path}:{lineno}: matches {pattern!r}: {line}")
|
|
69
|
+
if failed:
|
|
70
|
+
print(
|
|
71
|
+
"\nInternal research references do not belong in library code. "
|
|
72
|
+
f"Rephrase self-contained, or append '{WAIVER}' to a "
|
|
73
|
+
"deliberately functional line."
|
|
74
|
+
)
|
|
75
|
+
return 1
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
sys.exit(main(sys.argv[1:]))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|