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\n This notebook demonstrates how to use dapi's parameter sweep utilities to generate\n PyLauncher task lists and submit sweep jobs on DesignSafe.\n\n **PyLauncher** runs many independent serial tasks within a single SLURM allocation —\n ideal for parameter studies, Monte Carlo simulations, and batch processing.\n\n **What this notebook covers:**\n\n 1. **Generic demo** — a minimal `simulate.py` with `--alpha`/`--beta` parameters\n 2. **OpenSees demo** — cantilever pushover with `--NodalMass`/`--LCol` sweep"
207 },
218 {
229 "cell_type" : " code" ,
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 {
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 ]
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 " ,
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 {
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 " ,
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 " ,
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\n A real-world example using a 2D cantilever pushover analysis.\n We sweep over `NodalMass` and `LCol` (column length) to study how these structural\n parameters affect the pushover response.\n\n The 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\n An OpenSeesPy cantilever pushover script based on the\n [OpenSees Examples Manual](https://opensees.berkeley.edu/wiki/index.php/Examples_Manual).\n It accepts `--NodalMass`, `--LCol`, and `--outDir` as command-line arguments\n so 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" ,
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 " ,
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 " ,
439307 },
440308 "nbformat" : 4 ,
441309 "nbformat_minor" : 4
442- }
310+ }
0 commit comments