Skip to content

Commit 5b68734

Browse files
committed
Fix archive system
1 parent e411cc3 commit 5b68734

4 files changed

Lines changed: 46 additions & 159 deletions

File tree

dapi/client.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,23 +425,32 @@ def submit(
425425
cores_per_node: Optional[int] = None,
426426
max_minutes: Optional[int] = None,
427427
queue: Optional[str] = None,
428+
archive_system: Optional[str] = "designsafe",
429+
archive_path: Optional[str] = None,
428430
**kwargs,
429431
):
430432
"""Submit a PyLauncher sweep job.
431433
432434
Translates *directory* to a Tapis URI, builds a job request with
433435
``call_pylauncher.py`` as the script, and submits it.
434436
437+
Archives to the user's DesignSafe storage by default (not the
438+
app's archive path, which may belong to the app owner).
439+
435440
Args:
436441
directory: Path to the input directory containing
437442
``runsList.txt`` and ``call_pylauncher.py``
438443
(e.g. ``"/MyData/sweep/"``).
439-
app_id: Tapis application ID (e.g. ``"openseespy-s3"``).
444+
app_id: Tapis application ID (e.g. ``"designsafe-agnostic-app"``).
440445
allocation: TACC allocation to charge.
441446
node_count: Number of compute nodes.
442447
cores_per_node: Cores per node.
443448
max_minutes: Maximum runtime in minutes.
444449
queue: Execution queue name.
450+
archive_system: Archive system. Defaults to ``"designsafe"``
451+
(the user's own storage).
452+
archive_path: Archive directory path. If None, uses the
453+
default ``tapis-jobs-archive/`` under the user's MyData.
445454
**kwargs: Additional arguments passed to
446455
``ds.jobs.generate()``.
447456
@@ -459,6 +468,8 @@ def submit(
459468
max_minutes=max_minutes,
460469
queue=queue,
461470
allocation=allocation,
471+
archive_system=archive_system,
472+
archive_path=archive_path,
462473
**kwargs,
463474
)
464475
return jobs_module.submit_job_request(self._tapis, job_request)

docs/examples/pylauncher.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ ds.jobs.parametric_sweep.generate(
5959
```python
6060
job = ds.jobs.parametric_sweep.submit(
6161
"/MyData/pylauncher_demo/",
62-
app_id="agnostic",
62+
app_id="designsafe-agnostic-app",
6363
allocation="your_allocation",
6464
node_count=1,
6565
cores_per_node=48,
@@ -104,7 +104,7 @@ ds.jobs.parametric_sweep.generate(
104104

105105
job = ds.jobs.parametric_sweep.submit(
106106
"/MyData/opensees_sweep/",
107-
app_id="openseespy-s3",
107+
app_id="designsafe-agnostic-app",
108108
allocation="your_allocation",
109109
node_count=2,
110110
cores_per_node=48,
@@ -127,5 +127,5 @@ $WORK/sweep_$SLURM_JOB_ID/run_ALPHA_BETA
127127
## Notes
128128

129129
- **PyLauncher is NOT a dapi dependency** — it's pre-installed on TACC compute nodes. dapi only generates the input files.
130-
- **MPI is disabled** — PyLauncher's `ClassicLauncher` runs independent serial tasks. The apps used (`agnostic`, `openseespy-s3`) already have `isMpi: false`.
130+
- **MPI is disabled** — PyLauncher's `ClassicLauncher` runs independent serial tasks. The `designsafe-agnostic-app` already has `isMpi: false`.
131131
- **Works with any app** — OpenSees, Python, MATLAB, Fortran binaries. The task list is just shell commands.

docs/jobs.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,20 @@ print(f"Default Cores: {app_details.jobAttributes.coresPerNode}")
9595

9696
| Application | App ID | Description |
9797
|-------------|--------|-------------|
98+
| Agnostic | `designsafe-agnostic-app` | General-purpose Python/OpenSees/PyLauncher execution |
9899
| MATLAB | `matlab-r2023a` | MATLAB computational environment |
99100
| OpenSees | `opensees-express` | Structural analysis framework |
101+
| OpenSees MP | `opensees-mp-s3` | OpenSees parallel (MPI) analysis |
100102
| MPM | `mpm-s3` | Material Point Method simulations |
101103
| ADCIRC | `adcirc-v55` | Coastal circulation modeling |
102104
| LS-DYNA | `ls-dyna` | Explicit finite element analysis |
103105

106+
The **Agnostic App** (`designsafe-agnostic-app`) is DesignSafe's general-purpose app for running Python scripts, OpenSeesPy, and PyLauncher parameter sweeps on TACC systems. It supports:
107+
- Python 3.12 with OpenSeesPy pre-installed
108+
- PyLauncher for running many independent tasks in a single allocation
109+
- Configurable TACC module loading
110+
- Serial execution (`isMpi: false`) — ideal for PyLauncher workflows
111+
104112
## Job Submission
105113

106114
### Basic Job Submission
@@ -461,7 +469,7 @@ ds.jobs.parametric_sweep.generate(
461469
# Submit the job
462470
job = ds.jobs.parametric_sweep.submit(
463471
"/MyData/sweep_demo/",
464-
app_id="agnostic",
472+
app_id="designsafe-agnostic-app",
465473
allocation="your_allocation",
466474
node_count=1,
467475
cores_per_node=48,

examples/pylauncher_sweep.ipynb

Lines changed: 22 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,7 @@
33
{
44
"cell_type": "markdown",
55
"metadata": {},
6-
"source": [
7-
"# PyLauncher Parameter Sweeps with dapi\n",
8-
"\n",
9-
"This notebook demonstrates how to use dapi's parameter sweep utilities to generate\n",
10-
"PyLauncher task lists and submit sweep jobs on DesignSafe.\n",
11-
"\n",
12-
"**PyLauncher** runs many independent serial tasks within a single SLURM allocation —\n",
13-
"ideal for parameter studies, Monte Carlo simulations, and batch processing.\n",
14-
"\n",
15-
"**What this notebook covers:**\n",
16-
"\n",
17-
"1. **Generic demo** — a minimal `simulate.py` with `--alpha`/`--beta` parameters\n",
18-
"2. **OpenSees demo** — Silvia Mazzoni's cantilever pushover with `--NodalMass`/`--LCol` sweep"
19-
]
6+
"source": "# PyLauncher Parameter Sweeps with dapi\n\nThis notebook demonstrates how to use dapi's parameter sweep utilities to generate\nPyLauncher task lists and submit sweep jobs on DesignSafe.\n\n**PyLauncher** runs many independent serial tasks within a single SLURM allocation —\nideal for parameter studies, Monte Carlo simulations, and batch processing.\n\n**What this notebook covers:**\n\n1. **Generic demo** — a minimal `simulate.py` with `--alpha`/`--beta` parameters\n2. **OpenSees demo** — cantilever pushover with `--NodalMass`/`--LCol` sweep"
207
},
218
{
229
"cell_type": "code",
@@ -34,9 +21,14 @@
3421
"outputs": [],
3522
"source": [
3623
"import os\n",
24+
"from pathlib import Path\n",
3725
"from dapi import DSClient\n",
3826
"\n",
39-
"ds = DSClient()"
27+
"ds = DSClient()\n",
28+
"\n",
29+
"# On DesignSafe JupyterHub, ~/MyData is /home/jupyter/MyData.\n",
30+
"# Locally, we use a local directory but the Tapis path stays /MyData/...\n",
31+
"MYDATA = Path(os.environ.get(\"JUPYTER_SERVER_ROOT\", os.path.expanduser(\"~\"))) / \"MyData\""
4032
]
4133
},
4234
{
@@ -49,7 +41,6 @@
4941
"\n",
5042
"A simple example sweeping over two parameters (`--alpha`, `--beta`). The script\n",
5143
"computes `result = alpha * beta`, writes it to a JSON output file, and prints a summary.\n",
52-
"This pattern works with any app — the commands in `runsList.txt` are just shell commands.\n",
5344
"\n",
5445
"### Write the script"
5546
]
@@ -60,8 +51,8 @@
6051
"metadata": {},
6152
"outputs": [],
6253
"source": [
63-
"input_dir_generic = os.path.expanduser(\"~/MyData/pylauncher_demo/\")\n",
64-
"os.makedirs(input_dir_generic, exist_ok=True)\n",
54+
"input_dir_generic = MYDATA / \"pylauncher_demo\"\n",
55+
"input_dir_generic.mkdir(parents=True, exist_ok=True)\n",
6556
"\n",
6657
"simulate_script = '''\\\n",
6758
"\"\"\"simulate.py — minimal demo script for PyLauncher parameter sweeps.\n",
@@ -91,10 +82,8 @@
9182
"print(f\"alpha={args.alpha}, beta={args.beta} -> result={result:.4f} written to {outfile}\")\n",
9283
"'''\n",
9384
"\n",
94-
"with open(os.path.join(input_dir_generic, \"simulate.py\"), \"w\") as f:\n",
95-
" f.write(simulate_script)\n",
96-
"\n",
97-
"print(f\"Wrote {input_dir_generic}simulate.py\")"
85+
"(input_dir_generic / \"simulate.py\").write_text(simulate_script)\n",
86+
"print(f\"Wrote {input_dir_generic}/simulate.py\")"
9887
]
9988
},
10089
{
@@ -152,18 +141,16 @@
152141
"commands = ds.jobs.parametric_sweep.generate(\n",
153142
" \"python3 simulate.py --alpha ALPHA --beta BETA --output out_ALPHA_BETA\",\n",
154143
" sweep,\n",
155-
" input_dir_generic,\n",
144+
" str(input_dir_generic),\n",
156145
" debug=\"host+job\",\n",
157146
")\n",
158147
"\n",
159148
"print(f\"Generated {len(commands)} task commands\\n\")\n",
160149
"print(\"=== runsList.txt ===\")\n",
161-
"with open(os.path.join(input_dir_generic, \"runsList.txt\")) as f:\n",
162-
" print(f.read())\n",
150+
"print((input_dir_generic / \"runsList.txt\").read_text())\n",
163151
"\n",
164152
"print(\"=== call_pylauncher.py ===\")\n",
165-
"with open(os.path.join(input_dir_generic, \"call_pylauncher.py\")) as f:\n",
166-
" print(f.read())\n",
153+
"print((input_dir_generic / \"call_pylauncher.py\").read_text())\n",
167154
"\n",
168155
"print(\"=== Files in input directory ===\")\n",
169156
"for fn in sorted(os.listdir(input_dir_generic)):\n",
@@ -187,7 +174,7 @@
187174
"source": [
188175
"# job = ds.jobs.parametric_sweep.submit(\n",
189176
"# \"/MyData/pylauncher_demo/\",\n",
190-
"# app_id=\"agnostic\",\n",
177+
"# app_id=\"designsafe-agnostic-app\",\n",
191178
"# allocation=\"your_allocation\",\n",
192179
"# node_count=1,\n",
193180
"# cores_per_node=48,\n",
@@ -199,131 +186,14 @@
199186
{
200187
"cell_type": "markdown",
201188
"metadata": {},
202-
"source": [
203-
"---\n",
204-
"\n",
205-
"## Part 2: OpenSees Cantilever Pushover Sweep\n",
206-
"\n",
207-
"A real-world example based on Silvia Mazzoni's cantilever pushover analysis.\n",
208-
"We sweep over `NodalMass` and `LCol` (column length) to study how these structural\n",
209-
"parameters affect the pushover response.\n",
210-
"\n",
211-
"The cantilever model:\n",
212-
"```\n",
213-
" ^Y\n",
214-
" |\n",
215-
" 2 __\n",
216-
" | |\n",
217-
" | |\n",
218-
" | |\n",
219-
" (1) LCol\n",
220-
" | |\n",
221-
" | |\n",
222-
" | |\n",
223-
" =1= ---- -------->X\n",
224-
"```\n",
225-
"\n",
226-
"- Node 1: fixed base\n",
227-
"- Node 2: free top with `NodalMass`\n",
228-
"- Elastic beam-column element\n",
229-
"- Gravity load (2000 kip downward) followed by lateral pushover (displacement-controlled)\n",
230-
"\n",
231-
"### Write the analysis script\n",
232-
"\n",
233-
"This is the OpenSeesPy cantilever pushover script adapted from\n",
234-
"[Silvia Mazzoni's example](https://opensees.berkeley.edu/wiki/index.php/Examples_Manual).\n",
235-
"It accepts `--NodalMass`, `--LCol`, and `--outDir` as command-line arguments\n",
236-
"so PyLauncher can run each parameter combination independently."
237-
]
189+
"source": "---\n\n## Part 2: OpenSees Cantilever Pushover Sweep\n\nA real-world example using a 2D cantilever pushover analysis.\nWe sweep over `NodalMass` and `LCol` (column length) to study how these structural\nparameters affect the pushover response.\n\nThe cantilever model:\n```\n ^Y\n |\n 2 __\n | |\n | |\n | |\n (1) LCol\n | |\n | |\n | |\n =1= ---- -------->X\n```\n\n- Node 1: fixed base\n- Node 2: free top with `NodalMass`\n- Elastic beam-column element\n- Gravity load (2000 kip downward) followed by lateral pushover (displacement-controlled)\n\n### Write the analysis script\n\nAn OpenSeesPy cantilever pushover script based on the\n[OpenSees Examples Manual](https://opensees.berkeley.edu/wiki/index.php/Examples_Manual).\nIt accepts `--NodalMass`, `--LCol`, and `--outDir` as command-line arguments\nso PyLauncher can run each parameter combination independently."
238190
},
239191
{
240192
"cell_type": "code",
241193
"execution_count": null,
242194
"metadata": {},
243195
"outputs": [],
244-
"source": [
245-
"input_dir_opensees = os.path.expanduser(\"~/MyData/opensees_sweep/\")\n",
246-
"os.makedirs(input_dir_opensees, exist_ok=True)\n",
247-
"\n",
248-
"cantilever_script = \"\"\"\\\n",
249-
"# Ex1a.Canti2D.Push — OpenSeesPy cantilever pushover\n",
250-
"# Adapted from Silvia Mazzoni & Frank McKenna, 2006/2020\n",
251-
"# Units: kip, inch, second\n",
252-
"#\n",
253-
"# Command-line arguments (set by PyLauncher per task):\n",
254-
"# --NodalMass mass at free node\n",
255-
"# --LCol column length\n",
256-
"# --outDir output directory for this run\n",
257-
"\n",
258-
"import argparse\n",
259-
"import os\n",
260-
"\n",
261-
"if os.path.exists(\"opensees.so\"):\n",
262-
" import opensees as ops\n",
263-
"else:\n",
264-
" import openseespy.opensees as ops\n",
265-
"\n",
266-
"parser = argparse.ArgumentParser()\n",
267-
"parser.add_argument(\"--NodalMass\", type=float, required=True)\n",
268-
"parser.add_argument(\"--LCol\", type=float, required=True)\n",
269-
"parser.add_argument(\"--outDir\", type=str, required=True)\n",
270-
"args = parser.parse_args()\n",
271-
"\n",
272-
"NodalMass = args.NodalMass\n",
273-
"LCol = args.LCol\n",
274-
"outDir = args.outDir\n",
275-
"\n",
276-
"os.makedirs(outDir, exist_ok=True)\n",
277-
"print(f\"Running: NodalMass={NodalMass}, LCol={LCol}, outDir={outDir}\")\n",
278-
"\n",
279-
"ops.wipe()\n",
280-
"ops.model(\"basic\", \"-ndm\", 2, \"-ndf\", 3)\n",
281-
"\n",
282-
"# Geometry\n",
283-
"ops.node(1, 0, 0)\n",
284-
"ops.node(2, 0, LCol)\n",
285-
"ops.fix(1, 1, 1, 1)\n",
286-
"ops.mass(2, NodalMass, 0.0, 0.0)\n",
287-
"\n",
288-
"# Element\n",
289-
"ops.geomTransf(\"Linear\", 1)\n",
290-
"ops.element(\"elasticBeamColumn\", 1, 1, 2, 3600000000, 4227, 1080000, 1)\n",
291-
"\n",
292-
"# Recorders\n",
293-
"ops.recorder(\"Node\", \"-file\", f\"{outDir}/DFree.out\", \"-time\", \"-node\", 2, \"-dof\", 1, 2, 3, \"disp\")\n",
294-
"ops.recorder(\"Node\", \"-file\", f\"{outDir}/RBase.out\", \"-time\", \"-node\", 1, \"-dof\", 1, 2, 3, \"reaction\")\n",
295-
"ops.recorder(\"Element\", \"-file\", f\"{outDir}/FCol.out\", \"-time\", \"-ele\", 1, \"globalForce\")\n",
296-
"\n",
297-
"# Gravity analysis\n",
298-
"ops.timeSeries(\"Linear\", 1)\n",
299-
"ops.pattern(\"Plain\", 1, 1)\n",
300-
"ops.load(2, 0.0, -2000.0, 0.0)\n",
301-
"ops.wipeAnalysis()\n",
302-
"ops.constraints(\"Plain\")\n",
303-
"ops.numberer(\"Plain\")\n",
304-
"ops.system(\"BandGeneral\")\n",
305-
"ops.test(\"NormDispIncr\", 1.0e-8, 6)\n",
306-
"ops.algorithm(\"Newton\")\n",
307-
"ops.integrator(\"LoadControl\", 0.1)\n",
308-
"ops.analysis(\"Static\")\n",
309-
"ops.analyze(10)\n",
310-
"ops.loadConst(\"-time\", 0.0)\n",
311-
"\n",
312-
"# Pushover analysis\n",
313-
"ops.timeSeries(\"Linear\", 2)\n",
314-
"ops.pattern(\"Plain\", 2, 2)\n",
315-
"ops.load(2, 2000.0, 0.0, 0.0)\n",
316-
"ops.integrator(\"DisplacementControl\", 2, 1, 0.1)\n",
317-
"ops.analyze(1000)\n",
318-
"\n",
319-
"print(f\"Done: NodalMass={NodalMass}, LCol={LCol}\")\n",
320-
"\"\"\"\n",
321-
"\n",
322-
"with open(os.path.join(input_dir_opensees, \"cantilever.py\"), \"w\") as f:\n",
323-
" f.write(cantilever_script)\n",
324-
"\n",
325-
"print(f\"Wrote {input_dir_opensees}cantilever.py\")"
326-
]
196+
"source": "input_dir_opensees = MYDATA / \"opensees_sweep\"\ninput_dir_opensees.mkdir(parents=True, exist_ok=True)\n\ncantilever_script = '''\\\n# Ex1a.Canti2D.Push — OpenSeesPy cantilever pushover\n# Based on the OpenSees Examples Manual\n# Units: kip, inch, second\n#\n# Command-line arguments (set by PyLauncher per task):\n# --NodalMass mass at free node\n# --LCol column length\n# --outDir output directory for this run\n\nimport argparse\nimport os\n\nif os.path.exists(\"opensees.so\"):\n import opensees as ops\nelse:\n import openseespy.opensees as ops\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--NodalMass\", type=float, required=True)\nparser.add_argument(\"--LCol\", type=float, required=True)\nparser.add_argument(\"--outDir\", type=str, required=True)\nargs = parser.parse_args()\n\nNodalMass = args.NodalMass\nLCol = args.LCol\noutDir = args.outDir\n\nos.makedirs(outDir, exist_ok=True)\nprint(f\"Running: NodalMass={NodalMass}, LCol={LCol}, outDir={outDir}\")\n\nops.wipe()\nops.model(\"basic\", \"-ndm\", 2, \"-ndf\", 3)\n\n# Geometry\nops.node(1, 0, 0)\nops.node(2, 0, LCol)\nops.fix(1, 1, 1, 1)\nops.mass(2, NodalMass, 0.0, 0.0)\n\n# Element\nops.geomTransf(\"Linear\", 1)\nops.element(\"elasticBeamColumn\", 1, 1, 2, 3600000000, 4227, 1080000, 1)\n\n# Recorders\nops.recorder(\"Node\", \"-file\", f\"{outDir}/DFree.out\", \"-time\", \"-node\", 2, \"-dof\", 1, 2, 3, \"disp\")\nops.recorder(\"Node\", \"-file\", f\"{outDir}/RBase.out\", \"-time\", \"-node\", 1, \"-dof\", 1, 2, 3, \"reaction\")\nops.recorder(\"Element\", \"-file\", f\"{outDir}/FCol.out\", \"-time\", \"-ele\", 1, \"globalForce\")\n\n# Gravity analysis\nops.timeSeries(\"Linear\", 1)\nops.pattern(\"Plain\", 1, 1)\nops.load(2, 0.0, -2000.0, 0.0)\nops.wipeAnalysis()\nops.constraints(\"Plain\")\nops.numberer(\"Plain\")\nops.system(\"BandGeneral\")\nops.test(\"NormDispIncr\", 1.0e-8, 6)\nops.algorithm(\"Newton\")\nops.integrator(\"LoadControl\", 0.1)\nops.analysis(\"Static\")\nops.analyze(10)\nops.loadConst(\"-time\", 0.0)\n\n# Pushover analysis\nops.timeSeries(\"Linear\", 2)\nops.pattern(\"Plain\", 2, 2)\nops.load(2, 2000.0, 0.0, 0.0)\nops.integrator(\"DisplacementControl\", 2, 1, 0.1)\nops.analyze(1000)\n\nprint(f\"Done: NodalMass={NodalMass}, LCol={LCol}\")\n'''\n\n(input_dir_opensees / \"cantilever.py\").write_text(cantilever_script)\nprint(f\"Wrote {input_dir_opensees}/cantilever.py\")"
327197
},
328198
{
329199
"cell_type": "markdown",
@@ -382,17 +252,15 @@
382252
"commands = ds.jobs.parametric_sweep.generate(\n",
383253
" \"python3 cantilever.py --NodalMass NODAL_MASS --LCol LCOL --outDir out_NODAL_MASS_LCOL\",\n",
384254
" sweep_opensees,\n",
385-
" input_dir_opensees,\n",
255+
" str(input_dir_opensees),\n",
386256
")\n",
387257
"\n",
388258
"print(f\"Generated {len(commands)} task commands\\n\")\n",
389259
"print(\"=== runsList.txt ===\")\n",
390-
"with open(os.path.join(input_dir_opensees, \"runsList.txt\")) as f:\n",
391-
" print(f.read())\n",
260+
"print((input_dir_opensees / \"runsList.txt\").read_text())\n",
392261
"\n",
393262
"print(\"=== call_pylauncher.py ===\")\n",
394-
"with open(os.path.join(input_dir_opensees, \"call_pylauncher.py\")) as f:\n",
395-
" print(f.read())\n",
263+
"print((input_dir_opensees / \"call_pylauncher.py\").read_text())\n",
396264
"\n",
397265
"print(\"=== Files in input directory ===\")\n",
398266
"for fn in sorted(os.listdir(input_dir_opensees)):\n",
@@ -416,7 +284,7 @@
416284
"source": [
417285
"# job = ds.jobs.parametric_sweep.submit(\n",
418286
"# \"/MyData/opensees_sweep/\",\n",
419-
"# app_id=\"openseespy-s3\",\n",
287+
"# app_id=\"designsafe-agnostic-app\",\n",
420288
"# allocation=\"your_allocation\",\n",
421289
"# node_count=1,\n",
422290
"# cores_per_node=48,\n",
@@ -439,4 +307,4 @@
439307
},
440308
"nbformat": 4,
441309
"nbformat_minor": 4
442-
}
310+
}

0 commit comments

Comments
 (0)