pfb-model-spec 0.0.1__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.
@@ -0,0 +1,5 @@
1
+ """Model specification for pfb-imaging"""
2
+
3
+ __version__ = "0.0.1"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1 @@
1
+ CONTAINER_IMAGE = "ghcr.io/landmanbester/pfb-model-spec:0.0.1"
@@ -0,0 +1,27 @@
1
+ """Cab definitions for generated Stimela cabs."""
2
+
3
+ from pathlib import Path
4
+
5
+ CAB_DIR = Path(__file__).parent
6
+ AVAILABLE_CABS = [p.stem for p in CAB_DIR.glob("*.yml")]
7
+
8
+
9
+ def get_cab_path(name: str) -> Path:
10
+ """Get path to a cab definition.
11
+
12
+ Args:
13
+ name: Name of the cab (without .yml extension)
14
+
15
+ Returns:
16
+ Path to the cab YAML file
17
+
18
+ Raises:
19
+ FileNotFoundError: If the cab doesn't exist
20
+ """
21
+ cab_path = CAB_DIR / f"{name}.yml"
22
+ if not cab_path.exists():
23
+ raise FileNotFoundError(f"Cab not found: {name}")
24
+ return cab_path
25
+
26
+
27
+ __all__ = ["CAB_DIR", "AVAILABLE_CABS", "get_cab_path"]
@@ -0,0 +1,10 @@
1
+ cabs:
2
+ onboard:
3
+ flavour: python
4
+ command: pfb_model_spec.core.onboard.onboard
5
+ name: onboard
6
+ info:
7
+ Print setup instructions for CI/CD, PyPI publishing, and GitHub configuration.
8
+ image: ghcr.io/landmanbester/pfb-model-spec:0.0.1
9
+ inputs: {}
10
+ outputs: {}
@@ -0,0 +1,23 @@
1
+ """CLI for pfb-model-spec."""
2
+
3
+ import typer
4
+
5
+ app = typer.Typer(
6
+ name="pfbspec",
7
+ help="Model specification for pfb-imaging",
8
+ no_args_is_help=True,
9
+ )
10
+
11
+
12
+ @app.callback()
13
+ def callback() -> None:
14
+ """Model specification for pfb-imaging"""
15
+ pass
16
+
17
+
18
+ # Register subcommands below. Imports go here (bottom) to avoid circular imports.
19
+ from pfb_model_spec.cli.onboard import onboard # noqa: E402
20
+
21
+ app.command(name="onboard")(onboard)
22
+
23
+ __all__ = ["app"]
@@ -0,0 +1,68 @@
1
+ from typing import Annotated, Literal
2
+
3
+ import typer
4
+ from hip_cargo import StimelaMeta, stimela_cab
5
+
6
+
7
+ @stimela_cab(
8
+ name="onboard",
9
+ info="Print setup instructions for CI/CD, PyPI publishing, and GitHub configuration.",
10
+ )
11
+ def onboard(
12
+ backend: Annotated[
13
+ Literal["auto", "native", "apptainer", "singularity", "docker", "podman"],
14
+ typer.Option(
15
+ help="Execution backend.",
16
+ ),
17
+ StimelaMeta(
18
+ skip=True,
19
+ ),
20
+ ] = "auto",
21
+ always_pull_images: Annotated[
22
+ bool,
23
+ typer.Option(
24
+ help="Always pull container images, even if cached locally.",
25
+ ),
26
+ StimelaMeta(
27
+ skip=True,
28
+ ),
29
+ ] = False,
30
+ ):
31
+ """
32
+ Print setup instructions for CI/CD, PyPI publishing, and GitHub configuration.
33
+ """
34
+ if backend == "native" or backend == "auto":
35
+ try:
36
+ # Pre-flight must_exist for remote URIs before dispatching.
37
+ from hip_cargo.utils.runner import preflight_remote_must_exist # noqa: E402
38
+
39
+ preflight_remote_must_exist(
40
+ onboard,
41
+ dict(),
42
+ )
43
+
44
+ # Lazy import the core implementation
45
+ from pfb_model_spec.core.onboard import onboard as onboard_core # noqa: E402
46
+
47
+ # Call the core function with all parameters
48
+ onboard_core()
49
+ return
50
+ except ImportError:
51
+ if backend == "native":
52
+ raise
53
+
54
+ # Resolve container image from installed package metadata
55
+ from hip_cargo.utils.config import get_container_image # noqa: E402
56
+ from hip_cargo.utils.runner import run_in_container # noqa: E402
57
+
58
+ image = get_container_image("pfb-model-spec")
59
+ if image is None:
60
+ raise RuntimeError("No Container URL in pfb-model-spec metadata.")
61
+
62
+ run_in_container(
63
+ onboard,
64
+ dict(),
65
+ image=image,
66
+ backend=backend,
67
+ always_pull_images=always_pull_images,
68
+ )
@@ -0,0 +1 @@
1
+ """Core implementations for pfb-model-spec."""
@@ -0,0 +1,181 @@
1
+ def onboard():
2
+ """Print setup instructions for CI/CD, PyPI publishing, and GitHub configuration."""
3
+ print(
4
+ """
5
+ ================================================================================
6
+ pfb-model-spec — Setup Instructions
7
+ ================================================================================
8
+
9
+ Follow these steps to complete the CI/CD and publishing setup for your project.
10
+
11
+ ────────────────────────────────────────────────────────────────────────────────
12
+ Step 1: Create a GitHub Repository
13
+ ────────────────────────────────────────────────────────────────────────────────
14
+
15
+ First, install the GitHub CLI (gh) if you haven't already:
16
+
17
+ # macOS
18
+ brew install gh
19
+
20
+ # Ubuntu/Debian
21
+ sudo apt install gh
22
+
23
+ # Other platforms: https://github.com/cli/cli#installation
24
+
25
+ Then authenticate:
26
+
27
+ gh auth login
28
+
29
+ Push your project to GitHub:
30
+
31
+ gh repo create landmanbester/pfb-model-spec --public --source=. --push
32
+
33
+ Or create the repo manually at https://github.com/new and push:
34
+
35
+ git remote add origin git@github.com:landmanbester/pfb-model-spec.git
36
+ git push -u origin main
37
+
38
+ ────────────────────────────────────────────────────────────────────────────────
39
+ Step 2: Set Up Trusted Publishing on PyPI
40
+ ────────────────────────────────────────────────────────────────────────────────
41
+
42
+ Use PyPI's "pending trusted publisher" feature to pre-authorize GitHub
43
+ Actions to publish your package. The PyPI project will be created
44
+ automatically on the first successful publish.
45
+
46
+ Go to: https://pypi.org/manage/account/publishing/
47
+
48
+ Scroll down to "Add a new pending publisher" and fill in:
49
+ PyPI project name: pfb-model-spec
50
+ Owner: landmanbester
51
+ Repository: pfb-model-spec
52
+ Workflow: publish.yml
53
+ Environment: pypi
54
+
55
+ ────────────────────────────────────────────────────────────────────────────────
56
+ Step 3: Create GitHub Environment
57
+ ────────────────────────────────────────────────────────────────────────────────
58
+
59
+ The publish workflow requires a GitHub environment named "pypi".
60
+
61
+ Go to: https://github.com/landmanbester/pfb-model-spec/settings/environments
62
+
63
+ Click "New environment" and name it: pypi
64
+
65
+ ────────────────────────────────────────────────────────────────────────────────
66
+ Step 4: Create a GitHub App (for Automated Cab Updates)
67
+ ────────────────────────────────────────────────────────────────────────────────
68
+
69
+ The update-cabs workflow needs to push commits to the repository. A GitHub
70
+ App is required so these commits can bypass branch protection rules.
71
+
72
+ a) Create the app:
73
+
74
+ Go to: https://github.com/settings/apps → "New GitHub App"
75
+
76
+ Settings:
77
+ Name: pfb-model-spec-bot (or any name you like)
78
+ Homepage: https://github.com/landmanbester/pfb-model-spec
79
+ Webhook: Uncheck "Active" (not needed)
80
+ Permissions: Repository → Contents → Read & write
81
+ Where: Only on this account
82
+
83
+ b) Generate a private key:
84
+
85
+ On the app page → "Generate a private key"
86
+ Save the downloaded .pem file.
87
+
88
+ c) Install the app on your repository:
89
+
90
+ On the app page → "Install App" → Select your repository.
91
+
92
+ d) Add secrets to your repository:
93
+
94
+ Go to: https://github.com/landmanbester/pfb-model-spec/settings/secrets/actions
95
+
96
+ Add two secrets:
97
+ APP_CLIENT_ID → The Client ID shown on the app's settings page
98
+ APP_PRIVATE_KEY → The contents of the .pem file you downloaded
99
+
100
+ ────────────────────────────────────────────────────────────────────────────────
101
+ Step 5: Set Up Branch Protection
102
+ ────────────────────────────────────────────────────────────────────────────────
103
+
104
+ Protect your default branch so all changes go through CI.
105
+
106
+ Go to: https://github.com/landmanbester/pfb-model-spec/settings/rules
107
+
108
+ Click "New ruleset" and configure:
109
+
110
+ Name: Protect main
111
+ Enforcement: Active
112
+ Target: Default branch
113
+ Rules to enable:
114
+ - Require a pull request before merging
115
+ - Require status checks to pass (add: "Code Quality", "Tests")
116
+ - Block force pushes
117
+
118
+ Bypass list:
119
+ - Add your GitHub App (created in Step 4) so it can push
120
+ automated cab updates
121
+
122
+ Make sure to click "Create" to save the ruleset.
123
+
124
+ ────────────────────────────────────────────────────────────────────────────────
125
+ Step 6: Make Your First Release
126
+ ────────────────────────────────────────────────────────────────────────────────
127
+
128
+ When you're ready to publish to PyPI:
129
+
130
+ uv run tbump 0.0.0
131
+
132
+ This will:
133
+ 1. Update version in pyproject.toml and __init__.py
134
+ 2. Regenerate cab definitions with the release version
135
+ 3. Commit, tag, and push
136
+ 4. GitHub Actions will build and publish to PyPI and ghcr.io
137
+
138
+ ================================================================================
139
+ That's it! Your CI/CD pipeline is fully configured.
140
+ ================================================================================
141
+
142
+ ────────────────────────────────────────────────────────────────────────────────
143
+ Day-to-Day Development: Image Tag Workflow
144
+ ────────────────────────────────────────────────────────────────────────────────
145
+
146
+ The container image is stored in src/pfb_model_spec/_container_image.py
147
+ as the single source of truth for cab generation and container fallback
148
+ execution. The tag portion must stay in sync with your current context.
149
+
150
+ When you create a feature branch:
151
+
152
+ 1. Edit src/pfb_model_spec/_container_image.py and change the tag:
153
+
154
+ CONTAINER_IMAGE = "ghcr.io/landmanbester/pfb-model-spec:my-feature"
155
+
156
+ 2. Commit and develop as normal — pre-commit hooks will generate cab
157
+ definitions with the correct branch-specific image tag.
158
+
159
+ You do NOT need to reset the tag before merging. On merge to main,
160
+ the update-cabs workflow automatically:
161
+
162
+ - Resets the CONTAINER_IMAGE tag to "latest"
163
+ - Regenerates cab definitions
164
+ - Commits _container_image.py and cab YAML files
165
+
166
+ During releases, tbump updates the tag to the semantic version
167
+ (e.g. 0.1.0) via its before-commit hooks.
168
+
169
+ ────────────────────────────────────────────────────────────────────────────────
170
+
171
+ NOTE: Once you've completed the setup steps above, you can safely delete the
172
+ onboard command (cli/onboard.py and core/onboard.py) and remove it from
173
+ cli/__init__.py.
174
+
175
+ Add your own commands following the same pattern — define a CLI function
176
+ with type hints and a @stimela_cab decorator, and Stimela cab definitions
177
+ will be auto-generated from your CLI definitions via pre-commit hooks.
178
+
179
+ For more details, see: https://github.com/landmanbester/hip-cargo#readme
180
+ """
181
+ )
@@ -0,0 +1,361 @@
1
+ import numpy as np
2
+ import sympy as sm
3
+ import xarray as xr
4
+ from scipy.interpolate import RegularGridInterpolator
5
+ from sympy.parsing.sympy_parser import parse_expr
6
+ from sympy.utilities.lambdify import lambdify
7
+
8
+ specs = ["genesis", "exodus"]
9
+
10
+
11
+ def fit_image_cube(time, freq, image, wgt=None, nbasist=None, nbasisf=None, method="poly", sigmasq=0):
12
+ """
13
+ Fit the time and frequency axes of an image cube where
14
+
15
+ time - (ntime) time axis
16
+ freq - (nband) frequency axis
17
+ image - (ntime, nband, nx, ny) pixelated image
18
+ wgt - (ntime, nband) optional per time and frequency weights
19
+ nbasist - number of time basis functions
20
+ nbasisf - number of frequency basis functions
21
+ method - method to use for fitting (poly or Legendre)
22
+ sigmasq - optional regularisation term to add to the Hessian
23
+ to improve conditioning
24
+
25
+ method:
26
+ poly - fit a monomials in time and frequency
27
+ Legendre - fit a Legendre polynomial in time and frequency
28
+
29
+ returns:
30
+ coeffs - fitted coefficients
31
+ x_index, y_index - pixel locations of non-zero pixels in the image
32
+ expr - a string representing the symbolic expression describing the fit
33
+ params - tuple of str, parameters to pass into function (excluding t and f)
34
+ tfunc - function which scales the time domain appropriately for method
35
+ ffunc - function which scales the frequency domain appropriately for method
36
+
37
+
38
+ The fit is performed in scaled coordinates (t=time/ref_time,f=freq/ref_freq)
39
+ """
40
+ ntime = time.size
41
+ nband = freq.size
42
+ ref_time = time[0]
43
+ ref_freq = freq[0]
44
+ import sympy as sm
45
+ from sympy.abc import a, f, t
46
+
47
+ if nbasist is None:
48
+ nbasist = ntime
49
+ else:
50
+ assert nbasist <= ntime
51
+ if nbasisf is None:
52
+ nbasisf = nband
53
+ else:
54
+ assert nbasisf <= nband
55
+
56
+ mask = np.any(image, axis=(0, 1)) # over t and f axes
57
+ x_index, y_index = np.where(mask)
58
+ ncomps = x_index.size
59
+
60
+ # components excluding zeros
61
+ beta = image[:, :, x_index, y_index].reshape(ntime * nband, ncomps)
62
+ if wgt is not None:
63
+ wgt = wgt.reshape(ntime * nband, 1)
64
+ else:
65
+ wgt = np.ones((ntime * nband, 1), dtype=float)
66
+
67
+ # nothing to fit
68
+ if ntime == 1 and nband == 1:
69
+ coeffs = beta
70
+ expr = a
71
+ params = (a,)
72
+ elif method == "poly":
73
+ wt = time / ref_time
74
+ tfunc = t / ref_time
75
+ xfit = np.tile(wt[:, None], (nband, nbasist)) ** np.arange(nbasist)
76
+ params = sm.symbols(f"t(0:{nbasist})")
77
+ expr = sum(co * t**i for i, co in enumerate(params))
78
+ # the costant offset will always be included since nbasist is at least one
79
+ if nband > 1:
80
+ wf = freq / ref_freq
81
+ ffunc = f / ref_freq
82
+ xf = np.tile(wf[:, None], (ntime, nbasisf - 1)) ** np.arange(1, nbasisf)
83
+ xfit = np.hstack((xfit, xf))
84
+ paramsf = sm.symbols(f"f(1:{nbasisf})")
85
+ expr += sum(co * f ** (i + 1) for i, co in enumerate(paramsf))
86
+ params += paramsf
87
+
88
+ elif method == "Legendre":
89
+ # scale to lie between -1,1 for stability
90
+ if ntime > 1:
91
+ tmax = time.max()
92
+ tmin = time.min()
93
+ wt = time - (tmax + tmin) / 2
94
+ wtmax = wt.max()
95
+ wt /= wtmax
96
+ # function to convert time to interp domain
97
+ tfunc = (t - (tmax + tmin) / 2) / wtmax
98
+ else:
99
+ wt = time
100
+ tfunc = t
101
+ xt = np.zeros((ntime, nbasist), dtype=float)
102
+ params = sm.symbols(f"t(0:{nbasist})")
103
+ if nbasist > 1:
104
+ expr = 0
105
+ for i in range(nbasist):
106
+ vals = np.polynomial.Legendre.basis(i)(wt)
107
+ xt[:, i] = vals
108
+ expr += sm.polys.orthopolys.legendre_poly(i, t) * params[i]
109
+ else:
110
+ xt[...] = 1.0
111
+ expr = params[0]
112
+ xfit = np.tile(xt, (nband, 1))
113
+ paramsf = sm.symbols(f"f(1:{nbasisf})")
114
+ if nband > 1:
115
+ xf = np.zeros((nband, nbasisf - 1))
116
+ fmax = freq.max()
117
+ fmin = freq.min()
118
+ wf = freq - (fmax + fmin) / 2
119
+ wfmax = wf.max()
120
+ wf /= wfmax
121
+ ffunc = (f - (fmax + fmin) / 2) / wfmax
122
+ for i in range(1, nbasisf):
123
+ vals = np.polynomial.Legendre.basis(i)(wf)
124
+ xf[:, i - 1] = vals
125
+ expr += sm.polys.orthopolys.legendre_poly(i, f) * paramsf[i - 1]
126
+ xf = np.tile(xf, (ntime, 1))
127
+ xfit = np.hstack((xfit, xf))
128
+ params += paramsf
129
+ else:
130
+ raise NotImplementedError(f"Method {method} not implemented")
131
+
132
+ dirty_coeffs = xfit.T.dot(wgt * beta)
133
+ hess_coeffs = xfit.T.dot(wgt * xfit)
134
+ # to improve conditioning
135
+ if sigmasq:
136
+ hess_coeffs += sigmasq * np.eye(hess_coeffs.shape[0])
137
+ coeffs = np.linalg.solve(hess_coeffs, dirty_coeffs)
138
+
139
+ return coeffs, x_index, y_index, str(expr), list(map(str, params)), str(tfunc), str(ffunc)
140
+
141
+
142
+ def fit_image_fscube(freq, image, wgt=None, nbasisf=None, method="Legendre", sigmasq=0):
143
+ """
144
+ Fit the frequency axis of an image cube where
145
+
146
+ freq - (nband,) frequency axis
147
+ image - (nband, ncorr, nx, ny) pixelated image
148
+ wgt - (nband, ncorr) optional per time and frequency weights
149
+ nbasisf - number of frequency basis functions
150
+ method - method to use for fitting (poly or Legendre)
151
+ sigmasq - optional regularisation term to add to the Hessian
152
+ to improve conditioning
153
+
154
+ method:
155
+ poly - fit a monomials to frequency axis
156
+ Legendre - fit a Legendre polynomial to frequency
157
+
158
+ returns:
159
+ coeffs - (ncorr, nbasisf, ncomps) fitted coefficients
160
+ x_index, y_index - (ncomps,) pixel locations of non-zero pixels in the image
161
+ expr - a string representing the symbolic expression describing the fit
162
+ params - tuple of str, parameters to pass into function (excluding t and f)
163
+ ffunc - function which scales the frequency domain appropriately for method
164
+ """
165
+ nband = freq.size
166
+ ref_freq = freq[0]
167
+ import sympy as sm
168
+ from sympy.abc import f
169
+
170
+ if nbasisf is None:
171
+ nbasisf = nband
172
+ else:
173
+ assert nbasisf <= nband
174
+
175
+ nband, ncorr, nx, ny = image.shape
176
+ mask = np.any(image, axis=(0, 1)) # over freq and corr axes
177
+ x_index, y_index = np.where(mask)
178
+ ncomps = x_index.size
179
+
180
+ # components excluding zeros
181
+ beta = image[:, :, x_index, y_index].reshape(nband, ncorr, ncomps)
182
+ if wgt is not None:
183
+ wgt = wgt.reshape(nband, ncorr, 1)
184
+ else:
185
+ wgt = np.ones((nband, ncorr, 1), dtype=float)
186
+
187
+ params = sm.symbols(f"f(0:{nbasisf})")
188
+ if nband == 1: # nothing to fit
189
+ coeffs = beta
190
+ expr = f
191
+ params = (f,)
192
+ elif method == "poly":
193
+ wf = freq / ref_freq
194
+ ffunc = f / ref_freq
195
+ xf = np.tile(wf[:, None], (1, nbasisf)) ** np.arange(nbasisf)
196
+ expr = sum(co * f**i for i, co in enumerate(params))
197
+
198
+ elif method == "Legendre":
199
+ xf = np.zeros((nband, nbasisf), dtype=float)
200
+ fmax = freq.max()
201
+ fmin = freq.min()
202
+ wf = freq - (fmax + fmin) / 2
203
+ wfmax = wf.max()
204
+ wf /= wfmax
205
+ ffunc = (f - (fmax + fmin) / 2) / wfmax
206
+ xf[:, 0] = 1.0
207
+ expr = params[0]
208
+ for i in range(1, nbasisf):
209
+ vals = np.polynomial.Legendre.basis(i)(wf)
210
+ xf[:, i] = vals
211
+ expr += sm.polys.orthopolys.legendre_poly(i, f) * params[i]
212
+ else:
213
+ raise NotImplementedError(f"Method {method} not implemented")
214
+
215
+ # fit each correlation separately
216
+ coeffs = np.zeros((ncorr, nbasisf, ncomps), dtype=beta.dtype)
217
+ for c in range(ncorr):
218
+ dirty_coeffs = xf.T.dot(wgt[:, c] * beta[:, c])
219
+ hess_coeffs = xf.T.dot(wgt[:, c] * xf)
220
+ # to improve conditioning
221
+ if sigmasq:
222
+ hess_coeffs += sigmasq * np.eye(hess_coeffs.shape[0])
223
+ coeffs[c] = np.linalg.solve(hess_coeffs, dirty_coeffs)
224
+
225
+ return coeffs, x_index, y_index, str(expr), list(map(str, params)), str(ffunc)
226
+
227
+
228
+ def eval_coeffs_to_cube(time, freq, nx, ny, coeffs, x_index, y_index, expr, paramf, texpr, fexpr):
229
+ ntime = time.size
230
+ nfreq = freq.size
231
+
232
+ image = np.zeros((ntime, nfreq, nx, ny), dtype=float)
233
+ params = sm.symbols(("t", "f"))
234
+ params += sm.symbols(tuple(paramf))
235
+ symexpr = parse_expr(expr)
236
+ modelf = lambdify(params, symexpr)
237
+ texpr = parse_expr(texpr)
238
+ tfunc = lambdify(params[0], texpr)
239
+ fexpr = parse_expr(fexpr)
240
+ ffunc = lambdify(params[1], fexpr)
241
+ for i, tval in enumerate(time):
242
+ for j, fval in enumerate(freq):
243
+ image[i, j, x_index, y_index] = modelf(tfunc(tval), ffunc(fval), *coeffs)
244
+
245
+ return image
246
+
247
+
248
+ def eval_coeffs_to_slice(
249
+ time,
250
+ freq,
251
+ coeffs,
252
+ x_index,
253
+ y_index,
254
+ expr,
255
+ paramf,
256
+ texpr,
257
+ fexpr,
258
+ nxi,
259
+ nyi,
260
+ cellxi,
261
+ cellyi,
262
+ x0i,
263
+ y0i,
264
+ nxo,
265
+ nyo,
266
+ cellxo,
267
+ cellyo,
268
+ x0o,
269
+ y0o,
270
+ ):
271
+ image_in = np.zeros((nxi, nyi), dtype=float)
272
+ params = sm.symbols(("t", "f"))
273
+ params += sm.symbols(tuple(paramf))
274
+ symexpr = parse_expr(expr)
275
+ modelf = lambdify(params, symexpr)
276
+ texpr = parse_expr(texpr)
277
+ tfunc = lambdify(params[0], texpr)
278
+ fexpr = parse_expr(fexpr)
279
+ ffunc = lambdify(params[1], fexpr)
280
+ image_in[x_index, y_index] = modelf(tfunc(time), ffunc(freq), *coeffs)
281
+
282
+ pix_area_in = cellxi * cellyi
283
+ pix_area_out = cellxo * cellyo
284
+ area_ratio = pix_area_out / pix_area_in
285
+
286
+ xin = (-(nxi // 2) + np.arange(nxi)) * cellxi + x0i
287
+ yin = (-(nyi // 2) + np.arange(nyi)) * cellyi + y0i
288
+ xo = (-(nxo // 2) + np.arange(nxo)) * cellxo + x0o
289
+ yo = (-(nyo // 2) + np.arange(nyo)) * cellyo + y0o
290
+
291
+ # how many pixels to pad by to extrapolate with zeros
292
+ xldiff = xin.min() - xo.min()
293
+ if xldiff > 0.0:
294
+ npadxl = int(np.ceil(xldiff / cellxi))
295
+ else:
296
+ npadxl = 0
297
+ yldiff = yin.min() - yo.min()
298
+ if yldiff > 0.0:
299
+ npadyl = int(np.ceil(yldiff / cellyi))
300
+ else:
301
+ npadyl = 0
302
+
303
+ xudiff = xo.max() - xin.max()
304
+ if xudiff > 0.0:
305
+ npadxu = int(np.ceil(xudiff / cellxi))
306
+ else:
307
+ npadxu = 0
308
+ yudiff = yo.max() - yin.max()
309
+ if yudiff > 0.0:
310
+ npadyu = int(np.ceil(yudiff / cellyi))
311
+ else:
312
+ npadyu = 0
313
+
314
+ do_pad = npadxl > 0
315
+ do_pad |= npadxu > 0
316
+ do_pad |= npadyl > 0
317
+ do_pad |= npadyu > 0
318
+ if do_pad:
319
+ image_in = np.pad(image_in, ((npadxl, npadxu), (npadyl, npadyu)), mode="constant")
320
+
321
+ xin = (-(nxi // 2 + npadxl) + np.arange(nxi + npadxl + npadxu)) * cellxi + x0i
322
+ nxi = nxi + npadxl + npadxu
323
+ yin = (-(nyi // 2 + npadyl) + np.arange(nyi + npadyl + npadyu)) * cellyi + y0i
324
+ nyi = nyi + npadyl + npadyu
325
+
326
+ do_interp = cellxi != cellxo
327
+ do_interp |= cellyi != cellyo
328
+ do_interp |= x0i != x0o
329
+ do_interp |= y0i != y0o
330
+ do_interp |= nxi != nxo
331
+ do_interp |= nyi != nyo
332
+ if do_interp:
333
+ interpo = RegularGridInterpolator((xin, yin), image_in, bounds_error=True, method="linear")
334
+ xx, yy = np.meshgrid(xo, yo, indexing="ij")
335
+ return interpo((xx, yy)) * area_ratio
336
+ else:
337
+ return image_in
338
+
339
+
340
+ def model_from_mds(mds_name, freqs=None):
341
+ """
342
+ Evaluate component model at the original resolution
343
+ """
344
+ mds = xr.open_zarr(mds_name, chunks=None)
345
+ if freqs is None:
346
+ freqs = mds.freqs.values
347
+ else:
348
+ freqs = np.atleast_1d(freqs)
349
+ return eval_coeffs_to_cube(
350
+ mds.times.values,
351
+ freqs,
352
+ mds.npix_x,
353
+ mds.npix_y,
354
+ mds.coefficients.values,
355
+ mds.location_x.values,
356
+ mds.location_y.values,
357
+ mds.parametrisation,
358
+ mds.params.values,
359
+ mds.texpr,
360
+ mds.fexpr,
361
+ )
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: pfb-model-spec
3
+ Version: 0.0.1
4
+ Summary: Model specification for pfb-imaging
5
+ Author: landmanbester
6
+ Author-email: landmanbester <lbester@sarao.ac.za>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: hip-cargo>=0.2.0
14
+ Requires-Dist: numpy ; extra == 'full'
15
+ Requires-Dist: scipy ; extra == 'full'
16
+ Requires-Dist: sympy ; extra == 'full'
17
+ Requires-Dist: xarray ; extra == 'full'
18
+ Requires-Python: >=3.10
19
+ Project-URL: Homepage, https://github.com/landmanbester/pfb-model-spec
20
+ Project-URL: Repository, https://github.com/landmanbester/pfb-model-spec
21
+ Project-URL: Bug Tracker, https://github.com/landmanbester/pfb-model-spec/issues
22
+ Provides-Extra: full
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pfb-model-spec
26
+
27
+ Model specification for pfb-imaging
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install pfb-model-spec
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ```bash
38
+ pfbspec --help
39
+ ```
@@ -0,0 +1,14 @@
1
+ pfb_model_spec/__init__.py,sha256=Z3wV_fwvYzgcGLu0NbF18YXJgq-fLbwidVT_UpFbAow,92
2
+ pfb_model_spec/_container_image.py,sha256=tpecVjNH_3lboB2nF-jQsrDdg0_GBiZSMfmtgSbNF_A,63
3
+ pfb_model_spec/cabs/__init__.py,sha256=7Jp91XKl-3QwF7sujSEetCay6gGwmv9EKFop6hVPpgw,634
4
+ pfb_model_spec/cabs/onboard.yml,sha256=1QjyKeabkUpUvoeKrhzQvy8pW1eFhd7apyXQmSlXKvQ,284
5
+ pfb_model_spec/cli/__init__.py,sha256=diaqF_mcnR69cdZHaQRhIGuCrYKQkv1vV6KgW65pfLo,459
6
+ pfb_model_spec/cli/onboard.py,sha256=lnFPZGoCUmo457I8NqZf2Saol2DFZJENHHOSv8NXKYY,1987
7
+ pfb_model_spec/core/__init__.py,sha256=lbBafo78iXfwsB4ndDWSh3VqDGw5dZArqqhXAe1MI2A,47
8
+ pfb_model_spec/core/onboard.py,sha256=YK-HrYLrvg53YwMxkmkR7cVCsGw4Fu54lnoEo90Z2d0,9064
9
+ pfb_model_spec/modelspec.py,sha256=v7xwUbKselmrn4YF_J_CxKGmwYamHEZ1wbkV_gS90lA,11796
10
+ pfb_model_spec-0.0.1.dist-info/licenses/LICENSE,sha256=F-fIq4GmaOnJVwQL8rn7V2e1onkioYJzOciwvUQct8A,1070
11
+ pfb_model_spec-0.0.1.dist-info/WHEEL,sha256=V5-3dKee3Zs8C4JP6swr6zdqriLsOpItBEQxe6_oWpY,81
12
+ pfb_model_spec-0.0.1.dist-info/entry_points.txt,sha256=IJkT-MkLtukIS1LIMvqr4UfK8VIVkArGBOCJJstahY0,52
13
+ pfb_model_spec-0.0.1.dist-info/METADATA,sha256=kPnSNqfnqbzHOUJsBCpANC57MTpaDyclYPG9vus1dis,1071
14
+ pfb_model_spec-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.18
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pfbspec = pfb_model_spec.cli:app
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 landmanbester
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.