dde-cargo 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.
dde_cargo/__init__.py ADDED
@@ -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))