htpolynet 2.0.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.
Files changed (110) hide show
  1. htpolynet/__init__.py +2 -0
  2. htpolynet/analysis/__init__.py +0 -0
  3. htpolynet/analysis/analyze.py +256 -0
  4. htpolynet/analysis/plot.py +588 -0
  5. htpolynet/analysis/postsim.py +444 -0
  6. htpolynet/analysis/utils.py +399 -0
  7. htpolynet/cli.py +488 -0
  8. htpolynet/core/__init__.py +0 -0
  9. htpolynet/core/bondtemplate.py +112 -0
  10. htpolynet/core/configuration.py +83 -0
  11. htpolynet/core/coordinates.py +644 -0
  12. htpolynet/core/molecule.py +998 -0
  13. htpolynet/core/projectfilesystem.py +616 -0
  14. htpolynet/core/runtime.py +774 -0
  15. htpolynet/core/topocoord.py +2044 -0
  16. htpolynet/core/topology.py +1138 -0
  17. htpolynet/cure/__init__.py +0 -0
  18. htpolynet/cure/chain.py +135 -0
  19. htpolynet/cure/curecontroller.py +746 -0
  20. htpolynet/cure/expandreactions.py +294 -0
  21. htpolynet/cure/reaction.py +280 -0
  22. htpolynet/external/__init__.py +0 -0
  23. htpolynet/external/ambertools.py +97 -0
  24. htpolynet/external/command.py +73 -0
  25. htpolynet/external/gromacs.py +308 -0
  26. htpolynet/external/slurm.py +111 -0
  27. htpolynet/external/software.py +183 -0
  28. htpolynet/geometry/__init__.py +0 -0
  29. htpolynet/geometry/bondlist.py +205 -0
  30. htpolynet/geometry/lattice.py +32 -0
  31. htpolynet/geometry/linkcell.py +210 -0
  32. htpolynet/geometry/matrix4.py +110 -0
  33. htpolynet/geometry/ring.py +274 -0
  34. htpolynet/io/__init__.py +6 -0
  35. htpolynet/io/gro.py +195 -0
  36. htpolynet/io/mol2.py +210 -0
  37. htpolynet/io/pdb.py +157 -0
  38. htpolynet/resources/README.md +20 -0
  39. htpolynet/resources/__init__.py +0 -0
  40. htpolynet/resources/cfg/DGE-PAC-hi.yaml +115 -0
  41. htpolynet/resources/cfg/DGE-PAC-lo.yaml +115 -0
  42. htpolynet/resources/cfg/FDE-DFDA-hi.yaml +116 -0
  43. htpolynet/resources/cfg/FDE-DFDA-lo.yaml +116 -0
  44. htpolynet/resources/cfg/GMASTY-hi.yaml +158 -0
  45. htpolynet/resources/cfg/GMASTY-lo.yaml +158 -0
  46. htpolynet/resources/cfg/README.md +1 -0
  47. htpolynet/resources/cfg/STY.yaml +23 -0
  48. htpolynet/resources/cfg/pMSTY-hi.yaml +101 -0
  49. htpolynet/resources/cfg/pMSTY-lo.yaml +101 -0
  50. htpolynet/resources/cfg/pSTY-hi.yaml +101 -0
  51. htpolynet/resources/cfg/pSTY-lo.yaml +101 -0
  52. htpolynet/resources/example_depot/0-liquid-styrene.sh +134 -0
  53. htpolynet/resources/example_depot/0-liquid-styrene.tgz +0 -0
  54. htpolynet/resources/example_depot/1-polystyrene.sh +202 -0
  55. htpolynet/resources/example_depot/1-polystyrene.tgz +0 -0
  56. htpolynet/resources/example_depot/2-bisgma-styrene-thermoset.sh +353 -0
  57. htpolynet/resources/example_depot/2-bisgma-styrene-thermoset.tgz +0 -0
  58. htpolynet/resources/example_depot/3-pacm-dgeba-epoxy-thermoset.sh +242 -0
  59. htpolynet/resources/example_depot/3-pacm-dgeba-epoxy-thermoset.tgz +0 -0
  60. htpolynet/resources/example_depot/4-dfda-fde-epoxy-thermoset.sh +216 -0
  61. htpolynet/resources/example_depot/4-dfda-fde-epoxy-thermoset.tgz +0 -0
  62. htpolynet/resources/mdp/README.md +1 -0
  63. htpolynet/resources/mdp/drag-min.mdp +13 -0
  64. htpolynet/resources/mdp/drag-npt.mdp +36 -0
  65. htpolynet/resources/mdp/drag-nvt.mdp +31 -0
  66. htpolynet/resources/mdp/min.mdp +14 -0
  67. htpolynet/resources/mdp/npt.mdp +37 -0
  68. htpolynet/resources/mdp/nvt.mdp +29 -0
  69. htpolynet/resources/mdp/relax-min.mdp +13 -0
  70. htpolynet/resources/mdp/relax-npt.mdp +37 -0
  71. htpolynet/resources/mdp/relax-nvt.mdp +31 -0
  72. htpolynet/resources/mdp/single-molecule-min.mdp +12 -0
  73. htpolynet/resources/mdp/single-molecule-nvt.mdp +25 -0
  74. htpolynet/resources/molecules/inputs/DFA.pdb +62 -0
  75. htpolynet/resources/molecules/inputs/DGE.mol2 +115 -0
  76. htpolynet/resources/molecules/inputs/EMB.mol2 +50 -0
  77. htpolynet/resources/molecules/inputs/FDE.pdb +72 -0
  78. htpolynet/resources/molecules/inputs/GMA.mol2 +163 -0
  79. htpolynet/resources/molecules/inputs/PAC.mol2 +91 -0
  80. htpolynet/resources/molecules/inputs/STY.mol2 +44 -0
  81. htpolynet/resources/molecules/make-monomers.sh +64 -0
  82. htpolynet/resources/molecules/pics/DFA.png +0 -0
  83. htpolynet/resources/molecules/pics/DGE.png +0 -0
  84. htpolynet/resources/molecules/pics/EMB.png +0 -0
  85. htpolynet/resources/molecules/pics/FDE.png +0 -0
  86. htpolynet/resources/molecules/pics/GMA.png +0 -0
  87. htpolynet/resources/molecules/pics/PAC.png +0 -0
  88. htpolynet/resources/molecules/pics/STY.png +0 -0
  89. htpolynet/resources/molecules/sample-inputs/DFA.pdb +62 -0
  90. htpolynet/resources/molecules/sample-inputs/DGE.mol2 +115 -0
  91. htpolynet/resources/molecules/sample-inputs/EMB.mol2 +50 -0
  92. htpolynet/resources/molecules/sample-inputs/FDE.pdb +72 -0
  93. htpolynet/resources/molecules/sample-inputs/GMA.mol2 +163 -0
  94. htpolynet/resources/molecules/sample-inputs/PAC.mol2 +91 -0
  95. htpolynet/resources/molecules/sample-inputs/STY.mol2 +44 -0
  96. htpolynet/resources/tcl/readbonds.tcl +32 -0
  97. htpolynet/resources/tcl/readgrx.tcl +46 -0
  98. htpolynet/resources/tcl/render.tcl +74 -0
  99. htpolynet/utils/__init__.py +0 -0
  100. htpolynet/utils/banner.py +34 -0
  101. htpolynet/utils/checkpoint.py +85 -0
  102. htpolynet/utils/dataframetools.py +92 -0
  103. htpolynet/utils/inputcheck.py +49 -0
  104. htpolynet/utils/logsetup.py +44 -0
  105. htpolynet/utils/stringthings.py +39 -0
  106. htpolynet-2.0.0.dist-info/METADATA +78 -0
  107. htpolynet-2.0.0.dist-info/RECORD +110 -0
  108. htpolynet-2.0.0.dist-info/WHEEL +4 -0
  109. htpolynet-2.0.0.dist-info/entry_points.txt +2 -0
  110. htpolynet-2.0.0.dist-info/licenses/LICENSE +21 -0
htpolynet/cli.py ADDED
@@ -0,0 +1,488 @@
1
+ """Manages the htpolynet application, provides the command-line interface entry point.
2
+
3
+ Author: Cameron F. Abrams <cfa22@drexel.edu>
4
+ """
5
+
6
+ import glob
7
+ import json
8
+ import logging
9
+ import os
10
+ import tarfile
11
+ import textwrap
12
+
13
+ import argparse as ap
14
+ import yaml
15
+
16
+ from importlib.metadata import distribution
17
+
18
+ from .analysis.analyze import analyze
19
+ from .analysis.plot import plots
20
+ from .analysis.postsim import postsim
21
+ from .core import projectfilesystem as pfs
22
+ from .core.runtime import Runtime
23
+ from .external import software
24
+ from .external.slurm import generate_script, _normalize_keys
25
+ from .utils.banner import banner_message
26
+ from .utils.inputcheck import input_check
27
+ from .utils.logsetup import setup_logging
28
+ from .utils.stringthings import my_logger
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def _is_editable_install():
34
+ """Returns True if htpolynet was installed in editable mode (pip install -e .)."""
35
+ try:
36
+ direct_url = distribution('htpolynet').read_text('direct_url.json')
37
+ if direct_url:
38
+ return json.loads(direct_url).get('dir_info', {}).get('editable', False)
39
+ except Exception:
40
+ pass
41
+ return False
42
+
43
+
44
+ def info(args: ap.Namespace):
45
+ """Handles the info subcommand.
46
+
47
+ Args:
48
+ args (argparse.Namespace): parsed arguments
49
+ """
50
+ print('This is some information on your installed version of htpolynet')
51
+ l = pfs.lib_setup()
52
+ software.sw_setup()
53
+ print(l.info())
54
+ print(software.to_string())
55
+ system_mols, cached_mols = pfs.get_molecule_info()
56
+ print('Available molecules in system library:')
57
+ for m in system_mols:
58
+ print(f' {m}')
59
+ if cached_mols:
60
+ print('Cached parameterized molecules:')
61
+ for m in cached_mols:
62
+ print(f' {m}')
63
+ possibles = l.get_example_names()
64
+ print('Available examples using htpolynet fetch-example')
65
+ for p in possibles:
66
+ if p[0].isdecimal():
67
+ print(f' {p}')
68
+
69
+ def run(args: ap.Namespace):
70
+ """Handles the run subcommand.
71
+
72
+ Args:
73
+ args (argparse.Namespace): parsed arguments
74
+ """
75
+ setup_logging(args.loglevel, diag=args.diag, no_banner=args.no_banner)
76
+ my_logger('htpolynet runtime begins', logger.info)
77
+ userlib = args.lib if os.path.exists(args.lib) else None
78
+ software.sw_setup()
79
+ my_logger(software.to_string(), logger.info)
80
+ pfs.pfs_setup(root=os.getcwd(), topdirs=pfs.Dirs.run_topdirs, verbose=True, projdir=args.proj, reProject=args.restart, userlibrary=userlib)
81
+ a = Runtime(cfgfile=args.config, restart=args.restart)
82
+ if args.param_only:
83
+ a.generate_molecules(force_checkin=args.force_checkin, force_parameterization=args.force_parameterization)
84
+ else:
85
+ a.do_workflow(force_checkin=args.force_checkin, force_parameterization=args.force_parameterization)
86
+ my_logger('htpolynet runtime ends', logger.info)
87
+
88
+ def _unpack_example(fullpath):
89
+ """Extracts a single example tarball into the current directory.
90
+
91
+ Args:
92
+ fullpath (str): absolute path to the .tgz file in the system library
93
+ """
94
+ with tarfile.open(fullpath) as tf:
95
+ tf.extractall('.')
96
+
97
+ def _fetch_one(depot, fullname):
98
+ """Fetches one example by name, preferring a .sh script over a .tgz tarball.
99
+
100
+ Args:
101
+ depot (str): path to the example depot directory
102
+ fullname (str): example name without extension
103
+ """
104
+ import shutil
105
+ sh_path = os.path.join(depot, f'{fullname}.sh')
106
+ tgz_path = os.path.join(depot, f'{fullname}.tgz')
107
+ if os.path.exists(sh_path):
108
+ dest = os.path.basename(sh_path)
109
+ shutil.copy(sh_path, dest)
110
+ print(f'Fetched {dest} (run with: bash {dest})')
111
+ elif os.path.exists(tgz_path):
112
+ _unpack_example(tgz_path)
113
+ else:
114
+ raise FileNotFoundError(f'No example found for {fullname}')
115
+
116
+ def fetch_example(args):
117
+ """Handles the fetch-example subcommand.
118
+
119
+ Args:
120
+ args (argparse.Namespace): parsed arguments
121
+ """
122
+ l = pfs.system()
123
+ depot = l.get_example_depot_location()
124
+ possibles = l.get_example_names()
125
+ if args.n == 'all':
126
+ for fullname in possibles:
127
+ _fetch_one(depot, fullname)
128
+ return
129
+ if args.n.isdecimal():
130
+ fullname = [x for x in possibles if x.startswith(args.n)][0]
131
+ else:
132
+ fullname = args.n
133
+ _fetch_one(depot, fullname)
134
+
135
+ def pack_example(args):
136
+ l = pfs.system()
137
+ requires = ['README.md', 'run.sh', 'lib/molecules']
138
+ for r in requires:
139
+ assert os.path.exists(r), f'{r}: not found.'
140
+ ls = glob.glob('*.yaml') + glob.glob('*.yml')
141
+ assert len(ls)>0, f'No yaml config file found.'
142
+ bn = os.path.basename(os.getcwd())
143
+ inspectname = bn.split('-')
144
+ if len(inspectname) > 0:
145
+ firstfield = inspectname[0]
146
+ if firstfield.isdigit():
147
+ existing_n = int(firstfield)
148
+ use_n = False
149
+ else:
150
+ use_n = True
151
+ n = args.n
152
+ overwrite = args.overwrite
153
+ existing_examples = l.get_example_names()
154
+ numbers = [int(x.split('-')[0]) for x in existing_examples]
155
+ if not overwrite:
156
+ assert not n in numbers, f'Choose an index other than {n}; an example with this index already exists.'
157
+ if not use_n:
158
+ assert not existing_n in numbers, f'Choose an index other than {n}; an example with this index already exists. To do this, rename the directory.'
159
+ if n == -1:
160
+ n = str(max(numbers) + 1)
161
+ if use_n:
162
+ newname = f'{n}-{os.path.basename(os.getcwd())}'
163
+ else:
164
+ newname = f'{os.path.basename(os.getcwd())}'
165
+ depot_location = l.get_example_depot_location()
166
+ outpath = os.path.join(depot_location, f'{newname}.tgz')
167
+ if overwrite and os.path.exists(outpath):
168
+ logger.debug(f'Warning: overwriting example {outpath}')
169
+
170
+ def _shallow(tarinfo):
171
+ # mirror --exclude="*/*/*/*/*": omit paths deeper than 4 components
172
+ from pathlib import Path
173
+ return tarinfo if len(Path(tarinfo.name).parts) <= 4 else None
174
+
175
+ with tarfile.open(outpath, 'w:gz') as tf:
176
+ for fname in ['README.md', 'run.sh'] + ls:
177
+ tf.add(fname, arcname=f'{bn}/{fname}', recursive=False)
178
+ tf.add('lib/molecules', arcname=f'{bn}/lib/molecules', filter=_shallow)
179
+ logger.debug(f'Packed {outpath} -- consider a pull request!')
180
+
181
+ def pack_example_sh(args):
182
+ """Generates a self-contained bash setup script for the current example directory.
183
+
184
+ Scans the current directory for YAML configs and lib/molecules/inputs/ files,
185
+ embeds them as heredocs, and writes the resulting .sh script to the example depot.
186
+
187
+ Args:
188
+ args (argparse.Namespace): parsed arguments
189
+ """
190
+ import textwrap as _tw
191
+
192
+ l = pfs.system()
193
+ yamls = sorted(glob.glob('*.yaml') + glob.glob('*.yml'))
194
+ assert yamls, 'No YAML config file found in the current directory.'
195
+ inputs_dir = 'lib/molecules/inputs'
196
+ assert os.path.isdir(inputs_dir), f'{inputs_dir}: not found.'
197
+
198
+ # Collect text-format monomer input files (mol2, pdb); skip binaries like png
199
+ input_files = sorted(
200
+ f for f in glob.glob(os.path.join(inputs_dir, '*'))
201
+ if os.path.splitext(f)[1].lower() in ('.mol2', '.pdb')
202
+ )
203
+
204
+ bn = os.path.basename(os.getcwd())
205
+ inspectname = bn.split('-')
206
+ firstfield = inspectname[0] if inspectname else ''
207
+ use_n = not firstfield.isdigit()
208
+ existing_n = int(firstfield) if not use_n else None
209
+
210
+ n = args.n
211
+ overwrite = args.overwrite
212
+ existing_examples = l.get_example_names()
213
+ numbers = [int(x.split('-')[0]) for x in existing_examples]
214
+ if not overwrite:
215
+ if n != -1:
216
+ assert n not in numbers, f'Index {n} already exists; use --overwrite or choose another.'
217
+ if not use_n:
218
+ assert existing_n not in numbers, (
219
+ f'Index {existing_n} (from directory name) already exists; '
220
+ f'use --overwrite or rename the directory.'
221
+ )
222
+ if n == -1:
223
+ n = max(numbers) + 1 if numbers else 0
224
+ newname = f'{n}-{bn}' if use_n else bn
225
+ display_n = n if use_n else existing_n
226
+
227
+ depot_location = l.get_example_depot_location()
228
+ outpath = os.path.join(depot_location, f'{newname}.sh')
229
+ if overwrite and os.path.exists(outpath):
230
+ logger.debug(f'Warning: overwriting {outpath}')
231
+
232
+ # Determine the primary config (first yaml alphabetically) for the --run line
233
+ primary_yaml = yamls[0]
234
+
235
+ lines = [
236
+ '#!/bin/bash',
237
+ f'# htpolynet -- Example {display_n} -- {bn}',
238
+ '#',
239
+ f'# Writes all input files into ./{newname}/ and optionally launches the build.',
240
+ '#',
241
+ '# Usage:',
242
+ f'# bash {newname}.sh # set up directory only',
243
+ f'# bash {newname}.sh --run # set up and launch htpolynet',
244
+ '#',
245
+ '# Generated by: htpolynet pack-example-sh',
246
+ '#',
247
+ '# Cameron F. Abrams — cfa22@drexel.edu',
248
+ '',
249
+ 'set -euo pipefail',
250
+ '',
251
+ f'EXDIR="{newname}"',
252
+ 'if [ -d "$EXDIR" ]; then',
253
+ ' echo "Error: directory \'$EXDIR\' already exists. Remove it first." >&2',
254
+ ' exit 1',
255
+ 'fi',
256
+ '',
257
+ 'mkdir -p "$EXDIR/lib/molecules/inputs" "$EXDIR/lib/molecules/parameterized"',
258
+ 'cd "$EXDIR"',
259
+ '',
260
+ ]
261
+
262
+ # Embed each monomer input file as a heredoc
263
+ if input_files:
264
+ lines.append('# ---------------------------------------------------------------------------')
265
+ lines.append('# Monomer input files')
266
+ lines.append('# ---------------------------------------------------------------------------')
267
+ for fpath in input_files:
268
+ dest = os.path.join('lib/molecules/inputs', os.path.basename(fpath))
269
+ token = f'HTPOLYNET_{os.path.basename(fpath).replace(".", "_").upper()}_EOF'
270
+ lines.append(f"cat > {dest} << '{token}'")
271
+ with open(fpath) as fh:
272
+ lines.append(fh.read().rstrip())
273
+ lines.append(token)
274
+ lines.append('')
275
+
276
+ # Embed each YAML config as a heredoc
277
+ lines.append('# ---------------------------------------------------------------------------')
278
+ lines.append('# Configuration file(s)')
279
+ lines.append('# ---------------------------------------------------------------------------')
280
+ for yaml_file in yamls:
281
+ token = f'HTPOLYNET_{yaml_file.replace(".", "_").upper()}_EOF'
282
+ lines.append(f"cat > {yaml_file} << '{token}'")
283
+ with open(yaml_file) as fh:
284
+ lines.append(fh.read().rstrip())
285
+ lines.append(token)
286
+ lines.append('')
287
+
288
+ # --run launcher
289
+ lines += [
290
+ '# ---------------------------------------------------------------------------',
291
+ '# Optionally launch the build',
292
+ '# ---------------------------------------------------------------------------',
293
+ 'if [[ "${1:-}" == "--run" ]]; then',
294
+ f' htpolynet run -diag diagnostics.log {primary_yaml} &> console.log',
295
+ 'else',
296
+ ' echo "Setup complete. To run:"',
297
+ f' echo " cd $EXDIR && htpolynet run -diag diagnostics.log {primary_yaml}"',
298
+ 'fi',
299
+ ]
300
+
301
+ script = '\n'.join(lines) + '\n'
302
+ with open(outpath, 'w') as fh:
303
+ fh.write(script)
304
+ logger.debug(f'Wrote {outpath} -- consider a pull request!')
305
+ print(f'Wrote {outpath}')
306
+
307
+
308
+ def _add_run_options(p, loglevel='debug'):
309
+ """Adds run options (no positional config) shared by run and gen-slurm-script."""
310
+ p.add_argument('-lib',type=str,default='lib',help='local user library of molecular structures and parameterizations')
311
+ p.add_argument('-proj',type=str,default='next',help='project directory; "next" generates the next available; otherwise creates or resumes (with -restart) a named directory')
312
+ p.add_argument('-diag',type=str,default='htpolynet_runtime_diagnostics.log',help='diagnostic log file')
313
+ p.add_argument('-restart',default=False,action='store_true',help='restart in latest proj dir')
314
+ p.add_argument('--no-banner',default=False,action='store_true',help='suppress the startup banner')
315
+ p.add_argument('--force-parameterization',default=False,action='store_true',help='force GAFF parameterization of any input mol2 structures')
316
+ p.add_argument('--force-checkin',default=False,action='store_true',help='force check-in of generated parameter files to the system library')
317
+ p.add_argument('--param-only',default=False,action='store_true',help='stop after parameterizing molecules without building the full system')
318
+ p.add_argument('--loglevel',type=str,default=loglevel,help='log level for the diagnostic file (debug|info)')
319
+
320
+
321
+ def _add_runtime_args(p, loglevel='debug'):
322
+ """Adds the common arguments shared by the run and parameterize subcommands."""
323
+ p.add_argument('config',type=str,default=None,help='input configuration file in YAML format')
324
+ _add_run_options(p, loglevel=loglevel)
325
+
326
+ def gen_slurm_script(args: ap.Namespace):
327
+ """Handles the gen-slurm-script subcommand.
328
+
329
+ Args:
330
+ args (argparse.Namespace): parsed arguments
331
+ """
332
+ # Load YAML config to pick up the optional slurm: section
333
+ slurm_cfg = {}
334
+ if os.path.exists(args.config):
335
+ with open(args.config) as fh:
336
+ cfg = yaml.safe_load(fh) or {}
337
+ slurm_cfg = _normalize_keys(cfg.get('slurm', {}))
338
+
339
+ # CLI overrides take precedence over values from the YAML slurm: section
340
+ cli_overrides = {
341
+ 'job_name': args.job_name,
342
+ 'partition': args.partition,
343
+ 'account': args.account,
344
+ 'nodes': args.nodes,
345
+ 'ntasks': args.ntasks,
346
+ 'ntasks_per_node': args.ntasks_per_node,
347
+ 'cpus_per_task': args.cpus_per_task,
348
+ 'time': args.time,
349
+ 'mem': args.mem,
350
+ 'gres': args.gres,
351
+ }
352
+ for k, v in cli_overrides.items():
353
+ if v is not None:
354
+ slurm_cfg[k] = v
355
+
356
+ # --sif / --apptainer-alias override YAML values
357
+ sif = args.sif or slurm_cfg.get('sif')
358
+ apptainer_alias = args.apptainer_alias or slurm_cfg.get('apptainer_alias', 'apptainer')
359
+
360
+ if not args.native and not sif:
361
+ raise SystemExit(
362
+ 'error: containerized mode requires --sif <path/to/image.sif> '
363
+ '(or a sif: entry in the slurm: section of the config file)'
364
+ )
365
+
366
+ run_args = {
367
+ 'lib': args.lib,
368
+ 'proj': args.proj,
369
+ 'diag': args.diag,
370
+ 'restart': args.restart,
371
+ 'no_banner': args.no_banner,
372
+ 'force_parameterization': args.force_parameterization,
373
+ 'force_checkin': args.force_checkin,
374
+ 'param_only': args.param_only,
375
+ 'loglevel': args.loglevel,
376
+ }
377
+
378
+ script = generate_script(
379
+ config_file=args.config,
380
+ slurm_cfg=slurm_cfg,
381
+ run_args=run_args,
382
+ native=args.native,
383
+ sif=sif,
384
+ apptainer_alias=apptainer_alias,
385
+ )
386
+
387
+ outfile = args.output or f'submit_{os.path.splitext(os.path.basename(args.config))[0]}.sh'
388
+ with open(outfile, 'w') as fh:
389
+ fh.write(script)
390
+ print(f'Wrote SLURM script: {outfile}')
391
+
392
+
393
+ def _add_gen_slurm_args(p):
394
+ """Adds arguments for the gen-slurm-script subcommand."""
395
+ p.add_argument('config', type=str, help='htpolynet YAML config file')
396
+ # run options (mirrors _add_run_options)
397
+ _add_run_options(p, loglevel='info')
398
+ # execution mode
399
+ p.add_argument('--native', default=False, action='store_true',
400
+ help='run htpolynet natively instead of via Apptainer container')
401
+ p.add_argument('--sif', type=str, default=None,
402
+ help='path to the Apptainer/Singularity .sif image (required unless --native)')
403
+ p.add_argument('--apptainer-alias', type=str, default=None, dest='apptainer_alias',
404
+ help='container runtime command (default: apptainer)')
405
+ # output
406
+ p.add_argument('-o', '--output', type=str, default=None,
407
+ help='output script filename (default: submit_<config>.sh)')
408
+ # common SLURM directives as CLI overrides
409
+ p.add_argument('--job-name', type=str, default=None, dest='job_name')
410
+ p.add_argument('--partition', type=str, default=None)
411
+ p.add_argument('--account', type=str, default=None)
412
+ p.add_argument('--nodes', type=int, default=None)
413
+ p.add_argument('--ntasks', type=int, default=None)
414
+ p.add_argument('--ntasks-per-node', type=int, default=None, dest='ntasks_per_node')
415
+ p.add_argument('--cpus-per-task', type=int, default=None, dest='cpus_per_task')
416
+ p.add_argument('--time', type=str, default=None)
417
+ p.add_argument('--mem', type=str, default=None)
418
+ p.add_argument('--gres', type=str, default=None)
419
+
420
+
421
+ def _add_analysis_args(p):
422
+ """Adds the common arguments shared by the postsim and analyze subcommands."""
423
+ p.add_argument('-proj',type=str,default='',nargs='+',help='name of project directory')
424
+ p.add_argument('-lib',type=str,default='lib',help='local user library of molecular structures and parameterizations')
425
+ p.add_argument('-ocfg',type=str,default='',help='original htpolynet config file used to generate project(s)')
426
+ p.add_argument('-cfg',type=str,default='',help='config file specifying the operations to perform')
427
+ p.add_argument('--no-banner',default=False,action='store_true',help='suppress the startup banner')
428
+ p.add_argument('--loglevel',type=str,default='info',help='log level for the diagnostic file (debug|info)')
429
+
430
+ def cli():
431
+ """Command-line interface."""
432
+ l = pfs.lib_setup()
433
+ example_names = l.get_example_names()
434
+ example_ids = [x.split('-')[0] for x in example_names]
435
+
436
+ subcommands = [
437
+ ('run', run, 'build a system using instructions in the config file and any required molecular structure inputs (use --param-only to stop after parameterization)'),
438
+ ('info', info, 'print version, software, and library information'),
439
+ ('plots', plots, 'generate plots summarizing aspects of the current completed build'),
440
+ ('fetch-example', fetch_example, 'fetch and unpack example(s): '+', '.join(f'"{x}"' for x in example_names)),
441
+ ('input-check', input_check, 'report the number of atoms that would be in the initial system based on config'),
442
+ ('postsim', postsim, 'perform specified post-cure MD simulations on final results in one or more project directories'),
443
+ ('analyze', analyze, "perform 'gmx <command>' style analyses specified in the config file"),
444
+ ('gen-slurm-script', gen_slurm_script, 'generate a SLURM submission script for running htpolynet on a cluster'),
445
+ ]
446
+ if _is_editable_install():
447
+ subcommands.append(('pack-example', pack_example, '(dev) pack current directory as a tarball into resources/example_depot'))
448
+ subcommands.append(('pack-example-sh', pack_example_sh, '(dev) generate a self-contained bash setup script from the current example directory'))
449
+
450
+ parser = ap.ArgumentParser(description=textwrap.dedent(banner_message),formatter_class=ap.RawDescriptionHelpFormatter)
451
+ subparsers = parser.add_subparsers()
452
+ subparsers.required = True
453
+ cp = {}
454
+ for name,handler,help_str in subcommands:
455
+ cp[name] = subparsers.add_parser(name,help=help_str)
456
+ cp[name].set_defaults(func=handler)
457
+
458
+ _add_runtime_args(cp['run'])
459
+ _add_gen_slurm_args(cp['gen-slurm-script'])
460
+
461
+ cp['plots'].add_argument('source', type=str, choices=['diag', 'build', 'post'], default='build', help='source of data to plot')
462
+ cp['plots'].add_argument('--diags', type=str, default=[], nargs='+', help='names of diagnostic log files (1 or more)')
463
+ cp['plots'].add_argument('--proj', nargs='+', type=str, default=[], help='name of project director[y/ies]')
464
+ cp['plots'].add_argument('--cfg', type=str, nargs='+', default=[], help='name of input config files')
465
+ cp['plots'].add_argument('--buildplot', type=str, nargs='+', default=['t'], choices=['t', 'g', 'n', 'c'], help='build plot type: t=traces, g=2-D graph, n=homo-N between crosslinks, c=cluster-size distributions')
466
+ cp['plots'].add_argument('--traces', type=str, nargs='+', default=['t', 'd', 'p'], choices=['t', 'd', 'p'], help='trace types: t=temperature, d=density, p=potential energy')
467
+ cp['plots'].add_argument('--n_points', type=int, nargs=2, default=[10, 20], help='number of [cold-side,hot-side] data points for Tg line fits')
468
+ cp['plots'].add_argument('--plotfile', type=str, default='', help='name of output plot file')
469
+ cp['plots'].add_argument('--no-banner', default=False, action='store_true', help='suppress the startup banner')
470
+ cp['plots'].add_argument('--loglevel', type=str, default='info', help='log level for the diagnostic file (debug|info)')
471
+
472
+ cp['fetch-example'].add_argument('n', type=str, choices=example_ids+['all'], help='example to fetch: '+', '.join(example_names))
473
+
474
+ cp['input-check'].add_argument('config', type=str, default=None, help='input configuration file in YAML format')
475
+ cp['input-check'].add_argument('-lib', type=str, default='lib', help='local user library of molecular structures and parameterizations')
476
+
477
+ _add_analysis_args(cp['postsim'])
478
+ _add_analysis_args(cp['analyze'])
479
+
480
+ if 'pack-example' in cp:
481
+ cp['pack-example'].add_argument('-n', type=int, default=-1, help='desired index for this example (default: next available)')
482
+ cp['pack-example'].add_argument('--overwrite', default=False, action='store_true', help='overwrite any existing example with this index in the depot')
483
+ if 'pack-example-sh' in cp:
484
+ cp['pack-example-sh'].add_argument('-n', type=int, default=-1, help='desired index for this example (default: next available)')
485
+ cp['pack-example-sh'].add_argument('--overwrite', default=False, action='store_true', help='overwrite any existing example with this index in the depot')
486
+
487
+ args = parser.parse_args()
488
+ args.func(args)
File without changes
@@ -0,0 +1,112 @@
1
+ """Bond-context descriptor classes.
2
+
3
+ These are pure data objects that describe a bond by the chemical
4
+ context (atom names, residue names, order, bystanders, one-aways)
5
+ of its two participating atoms. They carry no cure-specific logic
6
+ and have no htpolynet dependencies.
7
+
8
+ Author: Cameron F. Abrams <cfa22@drexel.edu>
9
+ """
10
+ import logging
11
+
12
+ from copy import deepcopy
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BondTemplate:
18
+ def __init__(self, names, resnames, intraresidue, order, bystander_resnames, bystander_atomnames, oneaway_resnames, oneaway_atomnames):
19
+ """Creates a BondTemplate object.
20
+
21
+ Args:
22
+ names (list-like container of two ints): names of atoms in the bond
23
+ resnames (list-like container of two strs): names of the two residues to which the atoms belong
24
+ intraresidue (bool): True if this is an intraresidue bond
25
+ order (int): bond order (1=single, 2=double, ...)
26
+ bystander_resnames (list of two list-like containers of strs): lists of names of bystander residues (residues also bound to one of the atoms), one for each atom in names
27
+ bystander_atomnames (list of two list-like containers of strs): lists of names of atoms in bystander residues (parallel to bystander_resnames)
28
+ oneaway_resnames (list-like container of strs): names of one-away residues (residues bound one bond away from the new interresidue bond; only relevant for C=C free-radical polymerization)
29
+ oneaway_atomnames (list-like container of strs): names of atoms in one-away residues
30
+ """
31
+ self.names = names
32
+ self.resnames = resnames
33
+ self.intraresidue = intraresidue
34
+ self.bystander_resnames = bystander_resnames
35
+ self.bystander_atomnames = bystander_atomnames
36
+ self.oneaway_resnames = oneaway_resnames
37
+ self.oneaway_atomnames = oneaway_atomnames
38
+ self.order = order
39
+
40
+ def reverse(self):
41
+ """Reverses the order of all parallel lists in a BondTemplate object."""
42
+ self.names = self.names[::-1]
43
+ self.resnames = self.resnames[::-1]
44
+ self.bystander_resnames = self.bystander_resnames[::-1]
45
+ self.bystander_atomnames = self.bystander_atomnames[::-1]
46
+ self.oneaway_resnames = self.oneaway_resnames[::-1]
47
+ self.oneaway_atomnames = self.oneaway_atomnames[::-1]
48
+
49
+ def __str__(self):
50
+ return f'BondTemplate {self.names} resnames {self.resnames} intraresidue? {self.intraresidue} order {self.order} bystander-resnames {self.bystander_resnames} bystander-atomnames {self.bystander_atomnames} oneaway-resnames {self.oneaway_resnames} oneaway-atomnames {self.oneaway_atomnames}'
51
+
52
+ def __eq__(self,other):
53
+ check = self.names == other.names
54
+ check = check and self.intraresidue == other.intraresidue
55
+ check = check and self.resnames == other.resnames
56
+ check = check and self.bystander_resnames == other.bystander_resnames
57
+ check = check and self.bystander_atomnames == other.bystander_atomnames
58
+ check = check and self.oneaway_resnames == other.oneaway_resnames
59
+ check = check and self.oneaway_atomnames == other.oneaway_atomnames
60
+ return check
61
+
62
+ def is_reverse_of(self,other):
63
+ """Returns True if self and other are reverse of each other.
64
+
65
+ Args:
66
+ other (BondTemplate): another BondTemplate object
67
+
68
+ Returns:
69
+ bool: True if self and other are reverse copies of each other
70
+ """
71
+ rb = deepcopy(other)
72
+ rb.reverse()
73
+ return self == rb
74
+
75
+
76
+ BondTemplateList = list[BondTemplate]
77
+
78
+
79
+ class ReactionBond:
80
+ def __init__(self, idx, resids, order, bystanders, bystanders_atomidx, oneaways, oneaways_atomidx):
81
+ """Generates a new ReactionBond object.
82
+
83
+ Args:
84
+ idx (list-like container of two ints): indices of two atoms that form the bond
85
+ resids (list-like container of two ints): resids of the two atoms that form the bond
86
+ order (int): order of the bond (1=single, 2=double, ...)
87
+ bystanders (list of two list-like containers of ints): two lists of indices of interresidue resids already bound to each atom in idx
88
+ bystanders_atomidx (list of two list-like containers of ints): two lists of indices of interresidue atoms already bound to each atom in idx
89
+ oneaways (list of ints): list of one-away resids
90
+ oneaways_atomidx (list of ints): list of one-away atom indices
91
+ """
92
+ self.idx = idx
93
+ self.resids = resids
94
+ self.bystander_resids = bystanders
95
+ self.bystander_atomidx = bystanders_atomidx
96
+ self.oneaway_resids = oneaways
97
+ self.oneaway_atomidx = oneaways_atomidx
98
+ self.order = order
99
+
100
+ def reverse(self):
101
+ self.idx = self.idx[::-1]
102
+ self.resids = self.resids[::-1]
103
+ self.bystander_resids = self.bystander_resids[::-1]
104
+ self.bystander_atomidx = self.bystander_atomidx[::-1]
105
+ self.oneaway_resids = self.oneaway_resids[::-1]
106
+ self.oneaway_atomidx = self.oneaway_atomidx[::-1]
107
+
108
+ def __str__(self):
109
+ return f'ReactionBond {self.idx} resids {self.resids} order {self.order} bystander-resids {self.bystander_resids} oneaway-resids {self.oneaway_resids} oneaway-atomidx {self.oneaway_atomidx}'
110
+
111
+
112
+ ReactionBondList = list[ReactionBond]