nb2slurm 0.0.1.dev1__tar.gz → 0.0.1.dev2__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.
Files changed (39) hide show
  1. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/PKG-INFO +62 -2
  2. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/README.md +61 -1
  3. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/0_config.ipynb +191 -187
  4. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/1_build.ipynb +110 -109
  5. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/2_submit.ipynb +99 -95
  6. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/README.md +2 -2
  7. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/monte_carlo2slurm.ipynb +30 -4
  8. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/hpc-for-beginners.md +7 -2
  9. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/setup_notebooks.ipynb +3 -18
  10. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/walkthrough.ipynb +11 -11
  11. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/__init__.py +1 -1
  12. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/config.py +1 -0
  13. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/ssh.py +118 -24
  14. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/.claude/settings.local.json +0 -0
  15. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/.gitignore +0 -0
  16. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/CITATION.cff +0 -0
  17. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/LICENSE +0 -0
  18. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/Seamless.pdf +0 -0
  19. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/control/3_sync.ipynb +0 -0
  20. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/analyse_subsets.ipynb +0 -0
  21. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/jobs.json +0 -0
  22. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/notebooks/0_settings.ipynb +0 -0
  23. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/notebooks/1_simulate.ipynb +0 -0
  24. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/notebooks/2_plot.ipynb +0 -0
  25. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/docs/example_monte_carlo_pi/scripts/montecarlo.py +0 -0
  26. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/pyproject.toml +0 -0
  27. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/done.py +0 -0
  28. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/environment.py +0 -0
  29. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/render.py +0 -0
  30. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/runtime.py +0 -0
  31. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/settings.py +0 -0
  32. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/structure.py +0 -0
  33. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/cancel_jobs.sh.j2 +0 -0
  34. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/job.slurm.j2 +0 -0
  35. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/run_workflow.py.j2 +0 -0
  36. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/submit_batch.sh.j2 +0 -0
  37. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/templates/submit_jobs.sh.j2 +0 -0
  38. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/src/nb2slurm/workflow.py +0 -0
  39. {nb2slurm-0.0.1.dev1 → nb2slurm-0.0.1.dev2}/tests/test_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nb2slurm
3
- Version: 0.0.1.dev1
3
+ Version: 0.0.1.dev2
4
4
  Summary: Seamlessly scale a single-subject notebook workflow to many subjects on a SLURM HPC, driven entirely from notebooks.
5
5
  Project-URL: Homepage, https://github.com/eWaterCycle/nb2slurm
6
6
  Author: Mark Melotto
@@ -300,6 +300,41 @@ wf.status(ssh=cfg) # parsed squeue
300
300
  wf.cancel(ssh=cfg) # scancel what we submitted
301
301
  ```
302
302
 
303
+ ## Connecting over SSH
304
+
305
+ `SSHConfig` is how nb2slurm reaches the cluster — paramiko for commands, `rsync`
306
+ for file sync. You need an SSH key registered on the cluster. nb2slurm can make
307
+ the key, but **you** register it (most HPCs disable password login, so there's no
308
+ way for a tool to install it for you):
309
+
310
+ ```python
311
+ # 1. make a keypair locally (ed25519 recommended; key_type="rsa" also supported)
312
+ nb2slurm.generate_key(key_type="ed25519") # -> ~/.ssh/id_ed25519 (+.pub), prints the public key
313
+ # copy the printed PUBLIC key into your HPC's key-upload page / portal (SURF,
314
+ # Snellius, ...), or into ~/.ssh/authorized_keys on a login node. Reprint any
315
+ # time with nb2slurm.public_key("~/.ssh/id_ed25519").
316
+
317
+ # 2. describe the connection
318
+ cfg = nb2slurm.SSHConfig(
319
+ host="snellius.surf.nl", user="me",
320
+ remote_dir="/home/me/myproject",
321
+ key_filename="~/.ssh/id_ed25519",
322
+ # passphrase="…", # only if the key is encrypted AND you're not using ssh-agent
323
+ )
324
+
325
+ # 3. check it before anything else
326
+ cfg.test_connection() # runs hostname/whoami, prints a clear OK/FAIL (fails fast, never hangs)
327
+ ```
328
+
329
+ **Passphrase-protected keys.** The "password" most clusters prompt for is your
330
+ key's *passphrase* (decrypted locally), not a server login. The clean way is to
331
+ load the key into **ssh-agent** once — `ssh-add ~/.ssh/id_ed25519` — so both
332
+ nb2slurm *and* `rsync` (`push`/`pull`) authenticate through it with no prompts.
333
+ Passing `passphrase=` to `SSHConfig` also works, but it only unlocks the paramiko
334
+ calls (`test_connection`/`submit`/`status`/`check`); `push`/`pull` go through the
335
+ `rsync` CLI and still need the agent. Secrets (`password`, `passphrase`) are never
336
+ written to disk by `save_config`.
337
+
303
338
  ## Creating the conda environment + kernel
304
339
 
305
340
  The SLURM job does `conda activate <env>` and papermill needs a registered
@@ -326,6 +361,23 @@ Passing `environment=env` to `Workflow` keeps the names in sync (it errors if
326
361
  `kernel`/`conda_env` disagree) and makes `build()` also write `environment.yml`.
327
362
  `create_environment()` uses `mamba` when available, falls back to `conda`, and
328
363
  registers the kernel via `ipykernel`. Omit `ssh=` to build the env locally instead.
364
+ It runs **non-interactively** (it never stalls on a conda `[Y/n]` prompt over SSH)
365
+ and **streams** conda/mamba output live, so a multi-minute solve doesn't look like
366
+ a hang.
367
+
368
+ ### Rebuilding or removing an environment
369
+
370
+ `create_environment` is idempotent: re-running it *updates the env in place* (no
371
+ `Overwrite?` prompt). To recover from a half-built env, or force a clean slate:
372
+
373
+ ```python
374
+ wf.create_environment(ssh=cfg, overwrite=True) # delete, then rebuild from scratch
375
+ wf.remove_environment(ssh=cfg) # delete the env AND its Jupyter kernel
376
+ wf.environment.exists(ssh=cfg) # -> True/False, changes nothing
377
+ ```
378
+
379
+ (`Environment` has the same `.create(overwrite=...)`, `.remove()`, and `.exists()`
380
+ if you're driving one directly.)
329
381
 
330
382
  ### Using a cluster's existing environment (no env creation)
331
383
 
@@ -478,6 +530,11 @@ wf.submit([("NL","123","ssp126")], ssh=cfg) # override: run an explicit subset
478
530
  wf.submit(ssh=cfg, jobs_json="rerun.json") # override: use a different file
479
531
  ```
480
532
 
533
+ `concurrency` caps how many jobs run at once, chained with SLURM `afterany`
534
+ dependencies so you don't flood the queue. Set **`concurrency=0`** to submit
535
+ everything at once with **no dependencies** — ideal for a handful of quick,
536
+ independent jobs. You can also override it per call: `wf.submit(ssh=cfg, concurrency=0)`.
537
+
481
538
  Because each job's output dir is built from the JSON, your first notebook never
482
539
  builds folders — it just receives `outdir` and writes `settings.json`. The
483
540
  underlying parser is exposed as `nb2slurm.Structure` if you want it directly
@@ -501,7 +558,10 @@ The split is the safety mechanism:
501
558
  back can't overwrite a notebook you changed locally while jobs were running.
502
559
 
503
560
  So the normal loop after editing a notebook is: `push` the change, submit again
504
- (finished work is skipped via `done.csv`), then `pull` results when ready.
561
+ (finished work is skipped via `done.csv`), then `pull` results when ready. `pull`
562
+ is safe to run even before any results exist — it creates the remote `output/`
563
+ and `done/` dirs if they're missing rather than erroring, and is fine to re-run as
564
+ more jobs finish.
505
565
 
506
566
  ## Control notebooks
507
567
 
@@ -70,6 +70,41 @@ wf.status(ssh=cfg) # parsed squeue
70
70
  wf.cancel(ssh=cfg) # scancel what we submitted
71
71
  ```
72
72
 
73
+ ## Connecting over SSH
74
+
75
+ `SSHConfig` is how nb2slurm reaches the cluster — paramiko for commands, `rsync`
76
+ for file sync. You need an SSH key registered on the cluster. nb2slurm can make
77
+ the key, but **you** register it (most HPCs disable password login, so there's no
78
+ way for a tool to install it for you):
79
+
80
+ ```python
81
+ # 1. make a keypair locally (ed25519 recommended; key_type="rsa" also supported)
82
+ nb2slurm.generate_key(key_type="ed25519") # -> ~/.ssh/id_ed25519 (+.pub), prints the public key
83
+ # copy the printed PUBLIC key into your HPC's key-upload page / portal (SURF,
84
+ # Snellius, ...), or into ~/.ssh/authorized_keys on a login node. Reprint any
85
+ # time with nb2slurm.public_key("~/.ssh/id_ed25519").
86
+
87
+ # 2. describe the connection
88
+ cfg = nb2slurm.SSHConfig(
89
+ host="snellius.surf.nl", user="me",
90
+ remote_dir="/home/me/myproject",
91
+ key_filename="~/.ssh/id_ed25519",
92
+ # passphrase="…", # only if the key is encrypted AND you're not using ssh-agent
93
+ )
94
+
95
+ # 3. check it before anything else
96
+ cfg.test_connection() # runs hostname/whoami, prints a clear OK/FAIL (fails fast, never hangs)
97
+ ```
98
+
99
+ **Passphrase-protected keys.** The "password" most clusters prompt for is your
100
+ key's *passphrase* (decrypted locally), not a server login. The clean way is to
101
+ load the key into **ssh-agent** once — `ssh-add ~/.ssh/id_ed25519` — so both
102
+ nb2slurm *and* `rsync` (`push`/`pull`) authenticate through it with no prompts.
103
+ Passing `passphrase=` to `SSHConfig` also works, but it only unlocks the paramiko
104
+ calls (`test_connection`/`submit`/`status`/`check`); `push`/`pull` go through the
105
+ `rsync` CLI and still need the agent. Secrets (`password`, `passphrase`) are never
106
+ written to disk by `save_config`.
107
+
73
108
  ## Creating the conda environment + kernel
74
109
 
75
110
  The SLURM job does `conda activate <env>` and papermill needs a registered
@@ -96,6 +131,23 @@ Passing `environment=env` to `Workflow` keeps the names in sync (it errors if
96
131
  `kernel`/`conda_env` disagree) and makes `build()` also write `environment.yml`.
97
132
  `create_environment()` uses `mamba` when available, falls back to `conda`, and
98
133
  registers the kernel via `ipykernel`. Omit `ssh=` to build the env locally instead.
134
+ It runs **non-interactively** (it never stalls on a conda `[Y/n]` prompt over SSH)
135
+ and **streams** conda/mamba output live, so a multi-minute solve doesn't look like
136
+ a hang.
137
+
138
+ ### Rebuilding or removing an environment
139
+
140
+ `create_environment` is idempotent: re-running it *updates the env in place* (no
141
+ `Overwrite?` prompt). To recover from a half-built env, or force a clean slate:
142
+
143
+ ```python
144
+ wf.create_environment(ssh=cfg, overwrite=True) # delete, then rebuild from scratch
145
+ wf.remove_environment(ssh=cfg) # delete the env AND its Jupyter kernel
146
+ wf.environment.exists(ssh=cfg) # -> True/False, changes nothing
147
+ ```
148
+
149
+ (`Environment` has the same `.create(overwrite=...)`, `.remove()`, and `.exists()`
150
+ if you're driving one directly.)
99
151
 
100
152
  ### Using a cluster's existing environment (no env creation)
101
153
 
@@ -248,6 +300,11 @@ wf.submit([("NL","123","ssp126")], ssh=cfg) # override: run an explicit subset
248
300
  wf.submit(ssh=cfg, jobs_json="rerun.json") # override: use a different file
249
301
  ```
250
302
 
303
+ `concurrency` caps how many jobs run at once, chained with SLURM `afterany`
304
+ dependencies so you don't flood the queue. Set **`concurrency=0`** to submit
305
+ everything at once with **no dependencies** — ideal for a handful of quick,
306
+ independent jobs. You can also override it per call: `wf.submit(ssh=cfg, concurrency=0)`.
307
+
251
308
  Because each job's output dir is built from the JSON, your first notebook never
252
309
  builds folders — it just receives `outdir` and writes `settings.json`. The
253
310
  underlying parser is exposed as `nb2slurm.Structure` if you want it directly
@@ -271,7 +328,10 @@ The split is the safety mechanism:
271
328
  back can't overwrite a notebook you changed locally while jobs were running.
272
329
 
273
330
  So the normal loop after editing a notebook is: `push` the change, submit again
274
- (finished work is skipped via `done.csv`), then `pull` results when ready.
331
+ (finished work is skipped via `done.csv`), then `pull` results when ready. `pull`
332
+ is safe to run even before any results exist — it creates the remote `output/`
333
+ and `done/` dirs if they're missing rather than erroring, and is fine to re-run as
334
+ more jobs finish.
275
335
 
276
336
  ## Control notebooks
277
337
 
@@ -1,187 +1,191 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "markdown",
5
- "metadata": {},
6
- "source": [
7
- "# 0. Configure your run\n",
8
- "\n",
9
- "This is the only notebook you normally edit. Fill in the cells below, then run it\n",
10
- "top to bottom. It saves two files in the project root:\n",
11
- "\n",
12
- "* **`control_config.json`** — all your settings, read by `1_build`, `2_submit`\n",
13
- " and `3_sync` (so they always agree).\n",
14
- "* **`jobs.json`** — the nested list of jobs to run.\n",
15
- "\n",
16
- "Re-run this notebook whenever you change a setting."
17
- ]
18
- },
19
- {
20
- "cell_type": "markdown",
21
- "metadata": {},
22
- "source": [
23
- "## Project\n",
24
- "\n",
25
- "Name, the notebooks to run in order, and what varies per job."
26
- ]
27
- },
28
- {
29
- "cell_type": "code",
30
- "execution_count": null,
31
- "metadata": {},
32
- "outputs": [],
33
- "source": [
34
- "project_name = \"myproject\"\n",
35
- "notebooks = [\n",
36
- " \"notebooks/0_settings.ipynb\", # first notebook: writes settings.json\n",
37
- " \"notebooks/1_analysis.ipynb\",\n",
38
- " \"notebooks/2_more.ipynb\",\n",
39
- "]\n",
40
- "varying = [\"country\", \"region\", \"scenario\"] # the levels in jobs.json (below)"
41
- ]
42
- },
43
- {
44
- "cell_type": "markdown",
45
- "metadata": {},
46
- "source": "## The jobs to run\n\nA nested dict: each root-to-leaf path is one SLURM job, and the levels line up\nwith `varying` above. nb2slurm builds the matching output folders and submits one\njob per leaf.\n\n**Format:** a *dict* nests one more level, a *list* at the bottom means several\njobs sharing that parent, and `None`/`[]` ends the path. It's plain JSON, so for\nsmall runs type it by hand (below) and for big runs generate it in Python (a\ncomprehension, a CSV, an API query, ...) — see the README for the rules. The only\nthing that matters is that the finished dict lands in `jobs.json`."
47
- },
48
- {
49
- "cell_type": "code",
50
- "execution_count": null,
51
- "metadata": {},
52
- "outputs": [],
53
- "source": [
54
- "jobs = {\n",
55
- " \"NL\": {\"123\": [\"ssp126\", \"ssp245\"]},\n",
56
- " \"DE\": {\"789\": [\"ssp585\"]},\n",
57
- "}\n",
58
- "# -> jobs: (NL,123,ssp126) (NL,123,ssp245) (DE,789,ssp585)"
59
- ]
60
- },
61
- {
62
- "cell_type": "markdown",
63
- "metadata": {},
64
- "source": [
65
- "## HPC connection\n",
66
- "\n",
67
- "Your cluster login details. The project dir is derived so it can't drift."
68
- ]
69
- },
70
- {
71
- "cell_type": "code",
72
- "execution_count": null,
73
- "metadata": {},
74
- "outputs": [],
75
- "source": [
76
- "username_on_slurm = \"me\"\n",
77
- "slurm_host = \"spider.surf.nl\"\n",
78
- "project_dir_on_slurm = f\"/home/{username_on_slurm}/{project_name}\"\n",
79
- "ssh_key_file = \"~/.ssh/id_ed25519\""
80
- ]
81
- },
82
- {
83
- "cell_type": "markdown",
84
- "metadata": {},
85
- "source": [
86
- "## Resources per job\n",
87
- "\n",
88
- "What each SLURM job asks for, and where outputs go."
89
- ]
90
- },
91
- {
92
- "cell_type": "code",
93
- "execution_count": null,
94
- "metadata": {},
95
- "outputs": [],
96
- "source": "time_for_job_to_run = \"04:00:00\" # HH:MM:SS wall-clock limit\ncpus_per_job = 2\nnodes_per_job = 1\njobs_at_once = 3 # max running in parallel\noutput_directory = \"output\" # could be \"/scratch/me/output\""
97
- },
98
- {
99
- "cell_type": "markdown",
100
- "metadata": {},
101
- "source": "## Conda environment (optional)\n\nSkip if your cluster already provides Python (set `conda_env`/`setup` instead).\nOtherwise nb2slurm builds this environment + kernel for you in `1_build`.\n\n*Need a different environment for one or two notebooks (e.g. a calibration step)?*\nAdd `kernels={\"notebooks/step_8.ipynb\": \"myenv2\"}` and\n`extra_environments=[nb2slurm.Environment(name=\"myenv2\", kernel=\"myenv2\", ...)]`\nto the `Workflow` in the **Save** cell below — see the README for details. The\nsingle-environment setup here covers the common case."
102
- },
103
- {
104
- "cell_type": "code",
105
- "execution_count": null,
106
- "metadata": {},
107
- "outputs": [],
108
- "source": [
109
- "conda_env_name = \"myenv\"\n",
110
- "kernel_name = \"myenv\"\n",
111
- "conda_packages = [\"xarray\", \"numpy\"]\n",
112
- "pip_packages = [\"nb2slurm\", \"ewatercycle\"]"
113
- ]
114
- },
115
- {
116
- "cell_type": "markdown",
117
- "metadata": {},
118
- "source": [
119
- "## Data mounts (optional)\n",
120
- "\n",
121
- "rclone mounts the job should set up before running."
122
- ]
123
- },
124
- {
125
- "cell_type": "code",
126
- "execution_count": null,
127
- "metadata": {},
128
- "outputs": [],
129
- "source": [
130
- "data_mounts = [\n",
131
- " {\"remote\": \"dcache:/climate-data/caravan\", \"mountpoint\": \"/scratch/caravan\"},\n",
132
- "]"
133
- ]
134
- },
135
- {
136
- "cell_type": "markdown",
137
- "metadata": {},
138
- "source": [
139
- "## Save\n",
140
- "\n",
141
- "Builds the objects from your settings and writes `control_config.json` and\n",
142
- "`jobs.json`. The other notebooks load these — you don't edit them by hand."
143
- ]
144
- },
145
- {
146
- "cell_type": "code",
147
- "execution_count": null,
148
- "metadata": {},
149
- "outputs": [],
150
- "source": [
151
- "import json, pathlib, nb2slurm\n",
152
- "\n",
153
- "env = nb2slurm.Environment(\n",
154
- " name=conda_env_name, kernel=kernel_name,\n",
155
- " conda_packages=conda_packages, pip_packages=pip_packages,\n",
156
- ")\n",
157
- "wf = nb2slurm.Workflow(\n",
158
- " name=project_name, notebooks=notebooks, kernel=kernel_name,\n",
159
- " varying=varying, jobs_json=\"jobs.json\",\n",
160
- " resources=dict(nodes=nodes_per_job, cpus=cpus_per_job, time=time_for_job_to_run),\n",
161
- " mounts=data_mounts, concurrency=jobs_at_once,\n",
162
- " output_dir=output_directory, environment=env,\n",
163
- ")\n",
164
- "cfg = nb2slurm.SSHConfig(\n",
165
- " host=slurm_host, user=username_on_slurm,\n",
166
- " remote_dir=project_dir_on_slurm, key_filename=ssh_key_file,\n",
167
- ")\n",
168
- "\n",
169
- "pathlib.Path(\"jobs.json\").write_text(json.dumps(jobs, indent=2))\n",
170
- "nb2slurm.save_config(\"control_config.json\", workflow=wf, ssh=cfg)\n",
171
- "print(\"saved control_config.json and jobs.json\")"
172
- ]
173
- }
174
- ],
175
- "metadata": {
176
- "kernelspec": {
177
- "display_name": "Python 3",
178
- "language": "python",
179
- "name": "python3"
180
- },
181
- "language_info": {
182
- "name": "python"
183
- }
184
- },
185
- "nbformat": 4,
186
- "nbformat_minor": 5
187
- }
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# 0. Configure your run\n",
8
+ "\n",
9
+ "This is the only notebook you normally edit. Fill in the cells below, then run it\n",
10
+ "top to bottom. It saves two files in the project root:\n",
11
+ "\n",
12
+ "* **`control_config.json`** — all your settings, read by `1_build`, `2_submit`\n",
13
+ " and `3_sync` (so they always agree).\n",
14
+ "* **`jobs.json`** — the nested list of jobs to run.\n",
15
+ "\n",
16
+ "Re-run this notebook whenever you change a setting."
17
+ ]
18
+ },
19
+ {
20
+ "cell_type": "markdown",
21
+ "metadata": {},
22
+ "source": [
23
+ "## Project\n",
24
+ "\n",
25
+ "Name, the notebooks to run in order, and what varies per job."
26
+ ]
27
+ },
28
+ {
29
+ "cell_type": "code",
30
+ "execution_count": null,
31
+ "metadata": {},
32
+ "outputs": [],
33
+ "source": [
34
+ "project_name = \"myproject\"\n",
35
+ "notebooks = [\n",
36
+ " \"notebooks/0_settings.ipynb\", # first notebook: writes settings.json\n",
37
+ " \"notebooks/1_analysis.ipynb\",\n",
38
+ " \"notebooks/2_more.ipynb\",\n",
39
+ "]\n",
40
+ "varying = [\"country\", \"region\", \"scenario\"] # the levels in jobs.json (below)"
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "markdown",
45
+ "metadata": {},
46
+ "source": "## The jobs to run\n\nA nested dict: each root-to-leaf path is one SLURM job, and the levels line up\nwith `varying` above. nb2slurm builds the matching output folders and submits one\njob per leaf.\n\n**Format:** a *dict* nests one more level, a *list* at the bottom means several\njobs sharing that parent, and `None`/`[]` ends the path. It's plain JSON, so for\nsmall runs type it by hand (below) and for big runs generate it in Python (a\ncomprehension, a CSV, an API query, ...) — see the README for the rules. The only\nthing that matters is that the finished dict lands in `jobs.json`."
47
+ },
48
+ {
49
+ "cell_type": "code",
50
+ "execution_count": null,
51
+ "metadata": {},
52
+ "outputs": [],
53
+ "source": [
54
+ "jobs = {\n",
55
+ " \"NL\": {\"123\": [\"ssp126\", \"ssp245\"]},\n",
56
+ " \"DE\": {\"789\": [\"ssp585\"]},\n",
57
+ "}\n",
58
+ "# -> jobs: (NL,123,ssp126) (NL,123,ssp245) (DE,789,ssp585)"
59
+ ]
60
+ },
61
+ {
62
+ "cell_type": "markdown",
63
+ "metadata": {},
64
+ "source": [
65
+ "## HPC connection\n",
66
+ "\n",
67
+ "Your cluster login details. The project dir is derived so it can't drift."
68
+ ]
69
+ },
70
+ {
71
+ "cell_type": "code",
72
+ "execution_count": null,
73
+ "metadata": {},
74
+ "outputs": [],
75
+ "source": [
76
+ "username_on_slurm = \"me\"\n",
77
+ "slurm_host = \"spider.surf.nl\"\n",
78
+ "project_dir_on_slurm = f\"/home/{username_on_slurm}/{project_name}\"\n",
79
+ "ssh_key_file = \"~/.ssh/id_ed25519\"\n",
80
+ "\n",
81
+ "# No key yet? nb2slurm.generate_key(key_type=\"ed25519\") makes ~/.ssh/id_ed25519\n",
82
+ "# and prints the public key to register with your HPC. ssh-add it (or pass\n",
83
+ "# passphrase= to SSHConfig) if the key has a passphrase."
84
+ ]
85
+ },
86
+ {
87
+ "cell_type": "markdown",
88
+ "metadata": {},
89
+ "source": [
90
+ "## Resources per job\n",
91
+ "\n",
92
+ "What each SLURM job asks for, and where outputs go."
93
+ ]
94
+ },
95
+ {
96
+ "cell_type": "code",
97
+ "execution_count": null,
98
+ "metadata": {},
99
+ "outputs": [],
100
+ "source": "time_for_job_to_run = \"04:00:00\" # HH:MM:SS wall-clock limit\ncpus_per_job = 2\nnodes_per_job = 1\njobs_at_once = 3 # max running in parallel\noutput_directory = \"output\" # could be \"/scratch/me/output\""
101
+ },
102
+ {
103
+ "cell_type": "markdown",
104
+ "metadata": {},
105
+ "source": "## Conda environment (optional)\n\nSkip if your cluster already provides Python (set `conda_env`/`setup` instead).\nOtherwise nb2slurm builds this environment + kernel for you in `1_build`.\n\n*Need a different environment for one or two notebooks (e.g. a calibration step)?*\nAdd `kernels={\"notebooks/step_8.ipynb\": \"myenv2\"}` and\n`extra_environments=[nb2slurm.Environment(name=\"myenv2\", kernel=\"myenv2\", ...)]`\nto the `Workflow` in the **Save** cell below — see the README for details. The\nsingle-environment setup here covers the common case."
106
+ },
107
+ {
108
+ "cell_type": "code",
109
+ "execution_count": null,
110
+ "metadata": {},
111
+ "outputs": [],
112
+ "source": [
113
+ "conda_env_name = \"myenv\"\n",
114
+ "kernel_name = \"myenv\"\n",
115
+ "conda_packages = [\"xarray\", \"numpy\"]\n",
116
+ "pip_packages = [\"nb2slurm\", \"ewatercycle\"]"
117
+ ]
118
+ },
119
+ {
120
+ "cell_type": "markdown",
121
+ "metadata": {},
122
+ "source": [
123
+ "## Data mounts (optional)\n",
124
+ "\n",
125
+ "rclone mounts the job should set up before running."
126
+ ]
127
+ },
128
+ {
129
+ "cell_type": "code",
130
+ "execution_count": null,
131
+ "metadata": {},
132
+ "outputs": [],
133
+ "source": [
134
+ "data_mounts = [\n",
135
+ " {\"remote\": \"dcache:/climate-data/caravan\", \"mountpoint\": \"/scratch/caravan\"},\n",
136
+ "]"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "markdown",
141
+ "metadata": {},
142
+ "source": [
143
+ "## Save\n",
144
+ "\n",
145
+ "Builds the objects from your settings and writes `control_config.json` and\n",
146
+ "`jobs.json`. The other notebooks load these — you don't edit them by hand."
147
+ ]
148
+ },
149
+ {
150
+ "cell_type": "code",
151
+ "execution_count": null,
152
+ "metadata": {},
153
+ "outputs": [],
154
+ "source": [
155
+ "import json, pathlib, nb2slurm\n",
156
+ "\n",
157
+ "env = nb2slurm.Environment(\n",
158
+ " name=conda_env_name, kernel=kernel_name,\n",
159
+ " conda_packages=conda_packages, pip_packages=pip_packages,\n",
160
+ ")\n",
161
+ "wf = nb2slurm.Workflow(\n",
162
+ " name=project_name, notebooks=notebooks, kernel=kernel_name,\n",
163
+ " varying=varying, jobs_json=\"jobs.json\",\n",
164
+ " resources=dict(nodes=nodes_per_job, cpus=cpus_per_job, time=time_for_job_to_run),\n",
165
+ " mounts=data_mounts, concurrency=jobs_at_once,\n",
166
+ " output_dir=output_directory, environment=env,\n",
167
+ ")\n",
168
+ "cfg = nb2slurm.SSHConfig(\n",
169
+ " host=slurm_host, user=username_on_slurm,\n",
170
+ " remote_dir=project_dir_on_slurm, key_filename=ssh_key_file,\n",
171
+ ")\n",
172
+ "\n",
173
+ "pathlib.Path(\"jobs.json\").write_text(json.dumps(jobs, indent=2))\n",
174
+ "nb2slurm.save_config(\"control_config.json\", workflow=wf, ssh=cfg)\n",
175
+ "print(\"saved control_config.json and jobs.json\")"
176
+ ]
177
+ }
178
+ ],
179
+ "metadata": {
180
+ "kernelspec": {
181
+ "display_name": "Python 3",
182
+ "language": "python",
183
+ "name": "python3"
184
+ },
185
+ "language_info": {
186
+ "name": "python"
187
+ }
188
+ },
189
+ "nbformat": 4,
190
+ "nbformat_minor": 5
191
+ }