dde-cargo 0.1.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.
@@ -0,0 +1,29 @@
1
+ # Copyright (c) 2026 Tim Molteno (tim@elec.ac.nz)
2
+ name: Publish dde-cargo
3
+
4
+ on:
5
+ push:
6
+ tags: ["v*.*.*"]
7
+
8
+ jobs:
9
+ pypi:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ permissions:
13
+ contents: read
14
+ id-token: write
15
+
16
+ steps:
17
+ - name: Checkout repository
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v5
22
+
23
+ - name: Build
24
+ run: uv build
25
+
26
+ - name: Publish to PyPI
27
+ uses: pypa/gh-action-pypi-publish@release/v1
28
+ with:
29
+ packages-dir: dist
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ venv/
7
+
8
+ # Build artifacts
9
+ dist/
10
+ build/
11
+
12
+ # Tooling caches
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+ uv.lock.bak
16
+
17
+ # Editors
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+
22
+ # Session tooling
23
+ .zcode/
@@ -0,0 +1,126 @@
1
+ Metadata-Version: 2.5
2
+ Name: dde-cargo
3
+ Version: 0.1.0
4
+ Summary: Stimela cab definitions for DDFacet and killMS
5
+ Author-email: Tim Molteno <tim@elec.ac.nz>
6
+ License: GPLv3
7
+ Requires-Python: <3.14,>=3.10
8
+ Requires-Dist: pyparsing<3.3.3
9
+ Requires-Dist: stimela>=2.1.4
10
+ Description-Content-Type: text/markdown
11
+
12
+ # dde-cargo
13
+ Stimela cabs for DDFacet/Killms on both arm64 and x86-64 systems.
14
+
15
+ The cab YAMLs in `dde_cargo/` are verbatim copies of `meerkat_imaging/cabs/`
16
+ (`ddfacet.yml`, its two `_include`d parameter schemas, and the two
17
+ memory-plan cabs). A change to a cab starts in that repository; copy it here,
18
+ bump the version, and tag.
19
+
20
+ ## Cabs and images
21
+
22
+ | Cab | Image |
23
+ |---|---|
24
+ | `ddfacet` | `ghcr.io/tmolteno/ddfacet:v0.16.1` |
25
+ | `killms` | `ghcr.io/tmolteno/killms:v3.15.3` |
26
+ | `ddfacet-memory-plan` | `ghcr.io/tmolteno/ddfacet:v0.16.1` |
27
+ | `killms-memory-plan` | `ghcr.io/tmolteno/killms:v3.15.3` |
28
+ | `ddf_makemodel`, `ddf_makemask`, `dilatemask`, `clustercat`, `ddfacet-get-npix`, `bdsf.catalog`, `modelcat`, `autofacet`, `snapnodes` | `ghcr.io/tmolteno/ddfacet:v0.16.1` |
29
+
30
+ All images are published for amd64 and arm64.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ pip install dde-cargo
36
+ ```
37
+
38
+ Requires Python >= 3.10,< 3.14 (the same window as `skarabina-cargo`) and
39
+ installs `stimela>=2.1.4` — every cab runs in its container, so the host needs
40
+ nothing else.
41
+
42
+ ## Usage
43
+
44
+ In a stimela 2 recipe, load the cabs from the installed package with
45
+ configuratt's package-include syntax. The `cultcargo` include must come first:
46
+ `ddfacet.yml` carries an image-only override for cult-cargo's `bdsf.catalog`
47
+ cab (the upstream image is amd64-only; these cabs redirect it to the
48
+ multi-arch ddfacet image), and stimela builds every cab at load time, so
49
+ `bdsf.yml` must already be loaded when `ddfacet.yml` merges in:
50
+
51
+ ```yaml
52
+ _include:
53
+ - (cultcargo):
54
+ - bdsf.yml
55
+ - (dde_cargo):
56
+ - ddfacet.yml
57
+ - ddfacet-memory-plan.yml
58
+ - killms-memory-plan.yml
59
+ ```
60
+
61
+ then reference them from steps:
62
+
63
+ ```yaml
64
+ steps:
65
+ ddfacet-target:
66
+ cab: ddfacet
67
+ params:
68
+ Data.MS: mydata.ms
69
+ Output.Name: target0
70
+ killms-target:
71
+ cab: killms
72
+ params:
73
+ MSName: mydata.ms
74
+ InCol: CORRECTED_DATA
75
+ ```
76
+
77
+ Only `ddfacet.yml` is essential; the memory-plan files are optional extras
78
+ (wire them ahead of a step as shown in their header comments — e.g.
79
+ `ddfacet-memory-plan` suggests `Parallel.NCPU`, `Data.ChunkHours` and
80
+ `Selection.ChanStep` for the imaging step that follows).
81
+
82
+ Notes carried over from the upstream cab comments:
83
+
84
+ * The `autofacet` cab imports `auto_facet_tool/auto_facet.py` from the
85
+ *working directory*, so it needs a checkout of `meerkat_imaging` beside the
86
+ recipe (stimela binds the working directory into the container).
87
+ * The memory-plan cabs pin their image "to whatever the recipe uses": a recipe
88
+ with its own image var should override `image:` on the cab instead of
89
+ editing the package.
90
+ * The DDFacet cab wraps every parset namespace (`Data`, `Image`, `Facets`,
91
+ `Deconv`, ...) as dashed CLI options (`--Data-MS`, `--Image-NPix`, ...);
92
+ the killMS cab passes its flat parameter set through `kMS.py`.
93
+
94
+ ## Publishing
95
+
96
+ Trusted publishing (OIDC) — there is no API token. The GitHub Actions
97
+ workflow `.github/workflows/publish_pypi.yaml` fires on a `vX.Y.Z` tag, builds
98
+ with `uv`, and publishes from the `pypi` environment.
99
+
100
+ 1. Register the pending publisher on PyPI (once):
101
+ [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/)
102
+ with owner `tmolteno`, repository `dde-cargo`, workflow `publish_pypi.yaml`,
103
+ environment `pypi`. The workflow filename must match exactly.
104
+ 2. Bump `version` in `pyproject.toml`.
105
+ 3. `git commit -m "..." && git tag v0.1.0 && git push origin main --follow-tags`
106
+
107
+ The workflow run appears under the repo's Actions with the `pypi` environment;
108
+ the publish is live at [pypi.org/project/dde-cargo](https://pypi.org/project/dde-cargo/).
109
+
110
+ ## Development
111
+
112
+ ```sh
113
+ uv sync --all-groups
114
+ uv run pytest # schema contracts for the packaged cabs
115
+ uv build # wheel + sdist into dist/
116
+ ```
117
+
118
+ An end-to-end smoke check (loads the cabs through stimela's real spec loader,
119
+ as a recipe would):
120
+
121
+ ```sh
122
+ uv run stimela run /tmp/dde-cargo-smoke.yml dde-cargo-smoke -d
123
+ ```
124
+
125
+ with `/tmp/dde-cargo-smoke.yml` any recipe that `_include`s the package as
126
+ shown above and runs one or two of the cabs.
@@ -0,0 +1,115 @@
1
+ # dde-cargo
2
+ Stimela cabs for DDFacet/Killms on both arm64 and x86-64 systems.
3
+
4
+ The cab YAMLs in `dde_cargo/` are verbatim copies of `meerkat_imaging/cabs/`
5
+ (`ddfacet.yml`, its two `_include`d parameter schemas, and the two
6
+ memory-plan cabs). A change to a cab starts in that repository; copy it here,
7
+ bump the version, and tag.
8
+
9
+ ## Cabs and images
10
+
11
+ | Cab | Image |
12
+ |---|---|
13
+ | `ddfacet` | `ghcr.io/tmolteno/ddfacet:v0.16.1` |
14
+ | `killms` | `ghcr.io/tmolteno/killms:v3.15.3` |
15
+ | `ddfacet-memory-plan` | `ghcr.io/tmolteno/ddfacet:v0.16.1` |
16
+ | `killms-memory-plan` | `ghcr.io/tmolteno/killms:v3.15.3` |
17
+ | `ddf_makemodel`, `ddf_makemask`, `dilatemask`, `clustercat`, `ddfacet-get-npix`, `bdsf.catalog`, `modelcat`, `autofacet`, `snapnodes` | `ghcr.io/tmolteno/ddfacet:v0.16.1` |
18
+
19
+ All images are published for amd64 and arm64.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ pip install dde-cargo
25
+ ```
26
+
27
+ Requires Python >= 3.10,< 3.14 (the same window as `skarabina-cargo`) and
28
+ installs `stimela>=2.1.4` — every cab runs in its container, so the host needs
29
+ nothing else.
30
+
31
+ ## Usage
32
+
33
+ In a stimela 2 recipe, load the cabs from the installed package with
34
+ configuratt's package-include syntax. The `cultcargo` include must come first:
35
+ `ddfacet.yml` carries an image-only override for cult-cargo's `bdsf.catalog`
36
+ cab (the upstream image is amd64-only; these cabs redirect it to the
37
+ multi-arch ddfacet image), and stimela builds every cab at load time, so
38
+ `bdsf.yml` must already be loaded when `ddfacet.yml` merges in:
39
+
40
+ ```yaml
41
+ _include:
42
+ - (cultcargo):
43
+ - bdsf.yml
44
+ - (dde_cargo):
45
+ - ddfacet.yml
46
+ - ddfacet-memory-plan.yml
47
+ - killms-memory-plan.yml
48
+ ```
49
+
50
+ then reference them from steps:
51
+
52
+ ```yaml
53
+ steps:
54
+ ddfacet-target:
55
+ cab: ddfacet
56
+ params:
57
+ Data.MS: mydata.ms
58
+ Output.Name: target0
59
+ killms-target:
60
+ cab: killms
61
+ params:
62
+ MSName: mydata.ms
63
+ InCol: CORRECTED_DATA
64
+ ```
65
+
66
+ Only `ddfacet.yml` is essential; the memory-plan files are optional extras
67
+ (wire them ahead of a step as shown in their header comments — e.g.
68
+ `ddfacet-memory-plan` suggests `Parallel.NCPU`, `Data.ChunkHours` and
69
+ `Selection.ChanStep` for the imaging step that follows).
70
+
71
+ Notes carried over from the upstream cab comments:
72
+
73
+ * The `autofacet` cab imports `auto_facet_tool/auto_facet.py` from the
74
+ *working directory*, so it needs a checkout of `meerkat_imaging` beside the
75
+ recipe (stimela binds the working directory into the container).
76
+ * The memory-plan cabs pin their image "to whatever the recipe uses": a recipe
77
+ with its own image var should override `image:` on the cab instead of
78
+ editing the package.
79
+ * The DDFacet cab wraps every parset namespace (`Data`, `Image`, `Facets`,
80
+ `Deconv`, ...) as dashed CLI options (`--Data-MS`, `--Image-NPix`, ...);
81
+ the killMS cab passes its flat parameter set through `kMS.py`.
82
+
83
+ ## Publishing
84
+
85
+ Trusted publishing (OIDC) — there is no API token. The GitHub Actions
86
+ workflow `.github/workflows/publish_pypi.yaml` fires on a `vX.Y.Z` tag, builds
87
+ with `uv`, and publishes from the `pypi` environment.
88
+
89
+ 1. Register the pending publisher on PyPI (once):
90
+ [pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/)
91
+ with owner `tmolteno`, repository `dde-cargo`, workflow `publish_pypi.yaml`,
92
+ environment `pypi`. The workflow filename must match exactly.
93
+ 2. Bump `version` in `pyproject.toml`.
94
+ 3. `git commit -m "..." && git tag v0.1.0 && git push origin main --follow-tags`
95
+
96
+ The workflow run appears under the repo's Actions with the `pypi` environment;
97
+ the publish is live at [pypi.org/project/dde-cargo](https://pypi.org/project/dde-cargo/).
98
+
99
+ ## Development
100
+
101
+ ```sh
102
+ uv sync --all-groups
103
+ uv run pytest # schema contracts for the packaged cabs
104
+ uv build # wheel + sdist into dist/
105
+ ```
106
+
107
+ An end-to-end smoke check (loads the cabs through stimela's real spec loader,
108
+ as a recipe would):
109
+
110
+ ```sh
111
+ uv run stimela run /tmp/dde-cargo-smoke.yml dde-cargo-smoke -d
112
+ ```
113
+
114
+ with `/tmp/dde-cargo-smoke.yml` any recipe that `_include`s the package as
115
+ shown above and runs one or two of the cabs.
@@ -0,0 +1 @@
1
+ # Copyright (c) 2026 Tim Molteno (tim@elec.ac.nz)
@@ -0,0 +1,335 @@
1
+ # Copied verbatim from the installed package in ddfacet:v0.10.0.6
2
+ # (/opt/venv/lib/python3.13/site-packages/DDFacet/stimela/ddfacet-memory-plan.yml):
3
+ # v0.10.0.6 ships both the cab and the module, so nothing is vendored any more.
4
+ # The image pin follows the ddfacet pin (v0.12.1 since 2026-09-25): the planner is
5
+ # exercised on the same line the imaging runs, so its memory model matches.
6
+ # stimela cab: ddfacet-memory-plan
7
+ #
8
+ # Suggests --Parallel-NCPU, --Data-ChunkHours and (only when the band has to
9
+ # give) --Selection-ChanStep for a DDFacet run, from the parset that run will
10
+ # use and a memory budget. This is the command-line form of
11
+ # DDFacet/ToolsDir/ModEstimateMemory.py, and it ships with the package so a
12
+ # recipe can list it directly:
13
+ #
14
+ # - /opt/venv/lib/python3.13/site-packages/DDFacet/stimela/ddfacet-memory-plan.yml
15
+ #
16
+ # or copy the cab into the recipe's own cabs/ directory (that is what
17
+ # meerkat_imaging does with the other DDFacet cabs).
18
+ #
19
+ # Wiring it into a recipe, ahead of the imaging step:
20
+ #
21
+ # memory-plan:
22
+ # cab: ddfacet-memory-plan
23
+ # params:
24
+ # Parset: =steps.<something>.parset_out
25
+ # MS: =recipe.target-ms
26
+ # RamGB: 62
27
+ # ddfacet-target:
28
+ # params:
29
+ # Parallel.NCPU: =steps.memory-plan.NCPU
30
+ # Data.ChunkHours: =steps.memory-plan.ChunkHours
31
+ # Selection.ChanStep: =steps.memory-plan.ChanStep
32
+ #
33
+ # The estimate is +-40 % (see MANAGING_LIMITED_MEMORY.md): use it to choose
34
+ # between "NCPU 3" and "NCPU 8", not to plan within a gigabyte of the limit.
35
+ #
36
+ # THREADED (the rayon line, DDFacet >= v0.12): NCPU IS NOT A MEMORY KNOB ANY MORE.
37
+ # ModEstimateMemory's model is `main + NCPU x per-worker + chunk buffers`,
38
+ # calibrated on the process pool, where every worker held its own copies. On the
39
+ # rayon line the workers are threads in one process, and a sweep of the same
40
+ # dirty image on moist (8 cores, uv1500 fast profile, target 1, 2026-09-27)
41
+ # measured:
42
+ #
43
+ # NCPU ChunkHours wall CPU largest process
44
+ # 2 2.0 3:35 115% 13.8 GB
45
+ # 4 2.0 1:49 205% 13.0 GB
46
+ # 8 2.0 1:11 422% 13.3 GB
47
+ # 2 0.5 3:37 115% 13.7 GB
48
+ # 8 0.5 1:11 423% 13.3 GB
49
+ #
50
+ # i.e. flat in NCPU, 3x faster with every core -- while the model predicts
51
+ # 14.1 GB at NCPU 1 (right) and 2.18 GB per extra worker (wrong: 28.6 GB at 8),
52
+ # so with an 18 GB budget the unmodified planner answered `NCPU 1`. With
53
+ # `Threaded: true` (the default) the memory is therefore planned at ONE worker --
54
+ # ChunkHours and, if the band must give, ChanStep -- and NCPU is every usable
55
+ # core (or WantNCPU). `Threaded: false` restores the process-pool behaviour.
56
+ #
57
+ # RamGB and WantNCPU default to what the host actually offers when the step runs
58
+ # (MemAvailable, capped by a cgroup memory limit if there is one; the CPU
59
+ # affinity set), so the same recipe plans correctly on a 62 GB/12-core run box
60
+ # and on a 30 GB/8-core desktop. MemWatermarkGB is an output (15 % of the
61
+ # budget): wire it to --Parallel-MemWatermarkGB instead of a fixed value, which
62
+ # deadlocks a machine with less than that free (the pool workers pause for
63
+ # memory the parent holds while it waits for them).
64
+ #
65
+ # Bootstrapping, since a step cannot be planned from the parset it is about to
66
+ # write: plan each step from the parset of the *previous* run of that step (the
67
+ # pipeline's pattern), and for a fresh observation -- where no earlier parset
68
+ # exists -- pass the geometry instead of a parset (NPix, and optionally
69
+ # NFacets/NBand/OverS/Support/Nw/ChunkHoursIn). PlanFile is written by the
70
+ # command, so it is declared `must_exist: false`: a File input that stimela
71
+ # checks for existence would fail the step before the planner ran (DDFacet#17).
72
+
73
+ vars:
74
+ ddfacet-memory-plan:
75
+ image:
76
+ registry: ghcr.io/tmolteno
77
+ name: ddfacet
78
+ # Pin this to whatever the recipe uses; a recipe with its own image var
79
+ # should override `image:` on the cab instead.
80
+ version: v0.16.1
81
+
82
+ cabs:
83
+ ddfacet-memory-plan:
84
+ info: |
85
+ Suggest --Parallel-NCPU, --Data-ChunkHours and (only if the band has to
86
+ give) --Selection-ChanStep for a DDFacet run, from its parset and a
87
+ memory budget. Reads the MS for its row/channel/antenna counts when
88
+ one is given; pass NRows/NChan/NA instead when it cannot be read.
89
+ Needs an image carrying DDFacet/ToolsDir/ModEstimateMemory.py.
90
+ image:
91
+ _use: vars.ddfacet-memory-plan.image
92
+ inputs:
93
+ Parset:
94
+ dtype: File
95
+ required: false
96
+ info: |
97
+ the parset whose geometry is being planned (e.g. the parset_out
98
+ of a ddfacet step). Optional: with no parset, the geometry is
99
+ taken from the inputs below, which is the bootstrap case for a
100
+ fresh observation that has no earlier parset to plan from.
101
+ MS:
102
+ dtype: MS
103
+ required: false
104
+ info: measurement set to size the estimate with
105
+ RamGB:
106
+ dtype: float
107
+ required: false
108
+ info: >
109
+ memory the run may use. Default: what the host has available
110
+ when the step runs -- MemAvailable, capped by the cgroup memory
111
+ limit if there is one (a container or systemd scope)
112
+ NPix:
113
+ dtype: int
114
+ required: false
115
+ info: image size, when there is no Parset to read it from
116
+ NFacets:
117
+ dtype: int
118
+ required: false
119
+ NBand:
120
+ dtype: int
121
+ required: false
122
+ OverS:
123
+ dtype: int
124
+ required: false
125
+ Support:
126
+ dtype: int
127
+ required: false
128
+ Nw:
129
+ dtype: int
130
+ required: false
131
+ ChunkHoursIn:
132
+ dtype: float
133
+ required: false
134
+ info: the chunk size you intend, when planning without a Parset
135
+ default: 0.5
136
+ WantNCPU:
137
+ dtype: int
138
+ required: false
139
+ info: >
140
+ worker count you would like; also the cap. Default: the usable
141
+ cores (the CPU affinity set, so a taskset/cpuset is respected)
142
+ Threaded:
143
+ dtype: bool
144
+ default: true
145
+ info: >
146
+ the rayon line (DDFacet >= v0.12), where workers are threads in one
147
+ process and NCPU costs no measurable memory: plan the memory at one
148
+ worker and return every usable core as NCPU. false = the process
149
+ pool, where each worker costs memory (the model's original form)
150
+ Prefer:
151
+ dtype: str
152
+ default: fidelity
153
+ info: "'fidelity' keeps the full band while any worker count fits; 'speed' takes the workers first and subsamples the band if it must"
154
+ NDirections:
155
+ dtype: int
156
+ default: 1
157
+ NCorr:
158
+ dtype: int
159
+ default: 2
160
+ NRows:
161
+ dtype: int
162
+ required: false
163
+ NChan:
164
+ dtype: int
165
+ required: false
166
+ NA:
167
+ dtype: int
168
+ required: false
169
+ PlanOut:
170
+ dtype: File
171
+ default: ddfacet-memory-plan.json
172
+ # the command *writes* this file, so stimela must not require it
173
+ # to exist before the step starts (DDFacet#17)
174
+ must_exist: false
175
+ info: where to write the plan, as JSON
176
+ outputs:
177
+ NCPU:
178
+ dtype: int
179
+ info: pass to --Parallel-NCPU
180
+ ChunkHours:
181
+ dtype: float
182
+ info: pass to --Data-ChunkHours
183
+ ChanStep:
184
+ dtype: int
185
+ info: pass to --Selection-ChanStep
186
+ MemWatermarkGB:
187
+ dtype: float
188
+ info: >
189
+ pass to --Parallel-MemWatermarkGB: 15 % of the budget, so the
190
+ throttle scales with the machine instead of a fixed floor that
191
+ can exceed what is free (and then deadlocks)
192
+ RamGBUsed:
193
+ dtype: float
194
+ info: the budget the plan was made for (RamGB, or what the host offered)
195
+ PeakGB:
196
+ dtype: float
197
+ info: predicted peak as per-process accounting sees it (RSS sums / stimela's Mem GB)
198
+ HostRAMGB:
199
+ dtype: float
200
+ info: predicted peak as `free -g` sees it -- the shared arrays count as RAM only when /dev/shm is a tmpfs
201
+ Flags:
202
+ dtype: str
203
+ info: the three flags, ready to paste
204
+ Plan:
205
+ dtype: File
206
+ must_exist: false
207
+ info: the JSON plan (chosen settings, every combination that fits, notes)
208
+ flavour:
209
+ kind: python-code
210
+ command: |
211
+ import json
212
+ import math
213
+ from DDFacet.ToolsDir.ModEstimateMemory import suggestParameters
214
+
215
+ # Optional inputs may arrive unset, in which case stimela does not
216
+ # necessarily define the name at all -- look them up defensively.
217
+ _g = globals()
218
+
219
+
220
+ def _opt(name, default=None):
221
+ value = _g.get(name, default)
222
+ return default if value is None else value
223
+
224
+ # No Parset? Build the geometry from the inputs instead, so a fresh
225
+ # observation can be planned before any parset exists (DDFacet#17).
226
+ _geometry = {}
227
+ for _key in ("NPix", "NFacets", "NBand", "OverS", "Support", "Nw"):
228
+ _value = _opt(_key)
229
+ if _value is not None:
230
+ _geometry[_key] = _value
231
+ if _opt("Parset") is None and not _geometry:
232
+ raise RuntimeError(
233
+ "ddfacet-memory-plan: give either Parset or the geometry "
234
+ "(NPix, and optionally NFacets/NBand/OverS/Support/Nw)")
235
+ _pardict = None
236
+ if _opt("Parset") is None:
237
+ _pardict = {
238
+ "Image": {"NPix": _geometry.get("NPix", 5000)},
239
+ "Facets": {"NFacets": _geometry.get("NFacets", 3), "Padding": 1.7},
240
+ "Freq": {"NBand": _geometry.get("NBand", 1)},
241
+ "Output": {"NGridPol": 1},
242
+ "CF": {"OverS": _geometry.get("OverS", 11),
243
+ "Support": _geometry.get("Support", 7),
244
+ "Nw": _geometry.get("Nw", 100)},
245
+ "Data": {"ChunkHours": _opt("ChunkHoursIn", 0.5)},
246
+ "Parallel": {"NCPU": 1},
247
+ }
248
+ print("no Parset given: planning from the inputs (NPix %d, %d "
249
+ "facet(s), %d band(s))" % (_pardict["Image"]["NPix"],
250
+ _pardict["Facets"]["NFacets"],
251
+ _pardict["Freq"]["NBand"]))
252
+
253
+ import os
254
+
255
+
256
+ def _usable_cores():
257
+ try:
258
+ return len(os.sched_getaffinity(0))
259
+ except AttributeError:
260
+ return os.cpu_count() or 1
261
+
262
+
263
+ def _available_gb():
264
+ """MemAvailable, capped by the cgroup (v2) limit: what this run can use."""
265
+ avail = None
266
+ with open("/proc/meminfo") as _fh:
267
+ for _line in _fh:
268
+ if _line.startswith("MemAvailable:"):
269
+ avail = int(_line.split()[1]) * 1024
270
+ try:
271
+ _cg = open("/proc/self/cgroup").read().strip().split("::", 1)[-1]
272
+ _dir = "/sys/fs/cgroup" + _cg
273
+ _max = open(_dir + "/memory.max").read().strip()
274
+ if _max != "max":
275
+ _free = int(_max) - int(open(_dir + "/memory.current").read())
276
+ avail = _free if avail is None else min(avail, _free)
277
+ except (OSError, ValueError):
278
+ pass
279
+ return avail / float(1 << 30)
280
+
281
+
282
+ ram_gb = _opt("RamGB")
283
+ ram_source = "RamGB"
284
+ if ram_gb is None:
285
+ ram_gb = _available_gb()
286
+ ram_source = "available on this host"
287
+ cores = int(_opt("WantNCPU") or _usable_cores())
288
+ threaded = bool(_opt("Threaded", True))
289
+ print("budget %.1f GB (%s); %d core(s)%s; %s model"
290
+ % (ram_gb, ram_source, cores,
291
+ "" if _opt("WantNCPU") else " usable",
292
+ "threaded (memory planned at one worker)" if threaded
293
+ else "process-pool"))
294
+
295
+ # Threaded: the memory does not move with NCPU (measured flat, header),
296
+ # so the feasibility search runs at one worker and NCPU is every core.
297
+ suggestion = suggestParameters(
298
+ GD=_pardict, parset=_opt("Parset"), ms=_opt("MS"), ram_gb=ram_gb,
299
+ ncpu=1 if threaded else cores, max_ncpu=1 if threaded else cores,
300
+ prefer=_opt("Prefer", "fidelity"),
301
+ ndirections=_opt("NDirections", 1), ncorr=_opt("NCorr", 2),
302
+ nrows=_opt("NRows"), nchan=_opt("NChan"), na=_opt("NA"))
303
+ if threaded:
304
+ print("the memory model's own summary, at ONE worker (its NCPU line is "
305
+ "not the plan -- see 'plan:' below):")
306
+ print(suggestion)
307
+
308
+ NCPU = cores if threaded else int(suggestion.best["ncpu"])
309
+ ChunkHours = float(suggestion.best["chunk_hours"])
310
+ ChanStep = int(suggestion.best["chan_step"])
311
+ peak = float(suggestion.best["peak_gb"])
312
+ PeakGB = peak if math.isfinite(peak) else 0.0
313
+ host = float(suggestion.best.get("host_ram_gb", peak))
314
+ HostRAMGB = host if math.isfinite(host) else 0.0
315
+ MemWatermarkGB = float(max(1, round(0.15 * ram_gb)))
316
+ RamGBUsed = float(ram_gb)
317
+ Flags = " ".join(
318
+ (["--Selection-ChanStep %d" % ChanStep] if ChanStep != 1 else [])
319
+ + ["--Parallel-NCPU %d" % NCPU, "--Data-ChunkHours %g" % ChunkHours,
320
+ "--Parallel-MemWatermarkGB %g" % MemWatermarkGB])
321
+ print("plan: %s" % Flags)
322
+ Plan = _opt("PlanOut", "ddfacet-memory-plan.json")
323
+ with open(Plan, "w") as _fh:
324
+ json.dump(dict(ncpu=NCPU, chunk_hours=ChunkHours, chan_step=ChanStep,
325
+ peak_gb=PeakGB, host_ram_gb=HostRAMGB,
326
+ mem_watermark_gb=MemWatermarkGB, ram_gb=RamGBUsed,
327
+ ram_source=ram_source, threaded=threaded,
328
+ flags=Flags, notes=suggestion.notes,
329
+ feasible=[{k: v for k, v in f.items() if k != "estimate"}
330
+ for f in suggestion.feasible]),
331
+ _fh, indent=2)
332
+ if not suggestion.feasible:
333
+ raise RuntimeError(
334
+ "ddfacet-memory-plan: nothing in the search space fits %.1f GB "
335
+ "-- see the plan at %s" % (ram_gb, Plan))