Try on DesignSafe

designsafe-agnostic-app#

Dig Deep into an App Schema to Create the Input

by Silvia Mazzoni, DesignSafe, 2025

We are going to walk through the app schema to develop our job input, and then we will submit the job.

from tapipy.tapis import TapisResult
submitJob = False

Connect to Tapis#

t=OpsUtils.connect_tapis()
 -- Checking Tapis token --
 Token loaded from file. Token is still valid!
 Token expires at: 2026-02-05T18:54:18+00:00
 Token expires in: 1:51:46.626350
-- AUTHENTICATED VIA SAVED TOKEN --

Discover App#

# DesignSafe Agnostic App
# submit via web form: https://www.designsafe-ci.org/workspace/designsafe-agnostic-app

appId = 'designsafe-agnostic-app'
appVersion = 'latest'

Get app.json schema – input#

thisAppData_MP = OpsUtils.get_tapis_app_schema(t,appId,version=appVersion)
OpsUtils.display_tapis_app_schema(thisAppData_MP)
########################################
########### TAPIS-APP SCHEMA ###########
########################################
######## appID: designsafe-agnostic-app
######## version: 1.2.0
########################################
{
  sharedAppCtx: "silvia"
  isPublic: True
  tenant: "designsafe"
  id: "designsafe-agnostic-app"
  version: "1.2.0"
  description: "Agnostic Tapis App for General Python Execution as well as OpenSees, OpenSeesMP, OpenSeesSP, OpenSeesPy"
  owner: "silvia"
  enabled: True
  versionEnabled: True
  locked: False
  runtime: "ZIP"
  runtimeVersion: None
  runtimeOptions: None
  containerImage: "//work2/05072/silvia/stampede3/apps/designsafe-agnostic-app/1.2.0/designsafe-agnostic-app.zip"
  jobType: "BATCH"
  maxJobs: 2147483647
  maxJobsPerUser: 2147483647
  strictFileInputs: False
  uuid: "e3854c4d-470b-46d3-b9b6-eec487f8f23e"
  deleted: False
  created: "2026-02-05T16:13:21.989088Z"
  updated: "2026-02-05T16:13:21.989088Z"
  sharedWithUsers: []
  tags: ["portalName: DesignSafe", "portalName: CEP"]
  jobAttributes: {
    description: None
    dynamicExecSystem: False
    execSystemConstraints: None
    execSystemId: "stampede3"
    execSystemExecDir: "${JobWorkingDir}"
    execSystemInputDir: "${JobWorkingDir}"
    execSystemOutputDir: "${JobWorkingDir}"
    dtnSystemInputDir: "!tapis_not_set"
    dtnSystemOutputDir: "!tapis_not_set"
    execSystemLogicalQueue: "skx-dev"
    archiveSystemId: "designsafe.storage.default"
    archiveSystemDir: "silvia/tapis-jobs-archive/${JobCreateDate}/${JobName}-${JobUUID}"
    archiveOnAppError: True
    archiveMode: None
    isMpi: False
    mpiCmd: None
    cmdPrefix: None
    nodeCount: 1
    coresPerNode: 48
    memoryMB: 192000
    maxMinutes: 120
    fileInputs: [
      {
        name: "Input Directory"
        description: "Directory containing the main script and any supporting files (models, data, etc.). (Example: tapis://designsafe.storage.community/app_examples/opensees/OpenSeesPy)"
        inputMode: "REQUIRED"
        autoMountLocal: True
        envKey: "inputDirectory"
        sourceUrl: None
        targetPath: "inputDirectory"
        notes: {
          isHidden: False
          selectionMode: "directory"
        }
      }
    ]
    fileInputArrays: []
    subscriptions: []
    tags: []
    parameterSet: {
      appArgs: [
        {
          arg: "python3"
          name: "Main Program"
          description: "Binary executable to run. (e.g., OpenSees, OpenSeesMP, OpenSeesSP, python3 -- OpenSeesPy: use python3).    The executable must be available in the job's execution system. Some executables require you to load specific modules."
          inputMode: "REQUIRED"
          notes: {
            isHidden: False
            enum_values: [
              {
                OpenSees: "OpenSees"
              }
              {
                OpenSeesMP: "OpenSeesMP"
              }
              {
                OpenSeesSP: "OpenSeesSP"
              }
              {
                python3: "Python"
              }
            ]
          }
        }
        {
          arg: None
          name: "Main Script"
          description: "Filename (no path) of the input script passed to the executable (Example: Ex1a.Canti2D.Push.mpi4py.tacc.py). This file must reside in the Input Directory.  Note: This App uses TACC-Compiled OpenSeesPy: use 'import opensees' or 'import opensees as ops' in your script."
          inputMode: "REQUIRED"
          notes: {
            isHidden: False
            inputType: "fileInput"
          }
        }
        {
          arg: "True"
          name: "UseMPI"
          description: "Flag indicating whether the application should launch the main program with an MPI parallel-execution command (ibrun). **True**: enable distributed-memory parallelism, allowing multi-core or multi-node execution. (Suitable for OpenSeesMP / OpenSeesSP / Python with mpi4py (OpenSeesPy)). **False**: execution stays on one node. (Suitable for OpenSees, Python, or Python with concurrent.futures for one-node parallelism.)"
          inputMode: "REQUIRED"
          notes: {
            isHidden: False
            enum_values: [
              {
                True: "True — Enable MPI mode -- Use multi-node or multi-core parallelism."
              }
              {
                False: "False — No MPI -- Use single-node process."
              }
            ]
          }
        }
        {
          arg: None
          name: "CommandLine Arguments"
          description: "Optional command-line arguments appended after Main Script (e.g., '--npts 2000 --dir X' or any format consistent with how your input script parses them)."
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
      ]
      containerArgs: []
      schedulerOptions: [
        {
          arg: "--tapis-profile tacc-no-modules"
          name: "TACC Scheduler Profile"
          description: "Scheduler profile (e.g., tacc-no-modules) -- the app loads the modules you specify."
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
          }
        }
        {
          arg: None
          name: "TACC Reservation"
          description: "If you have a TACC reservation, enter the reservation string here."
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
      ]
      envVariables: [
        {
          key: "GET_TACC_OPENSEESPY"
          value: "True"
          description: "If 'True', use the TACC-compiled OpenSeesPy (not the PyPI wheel). In your script, import OpenSeesPy using 'import opensees' or 'import opensees as ops'."
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
            enum_values: [
              {
                True: "True: Copy TACC-Compiled OpenSeesPy"
              }
              {
                False: "False: no TACC-Compiled OpenSeesPy"
              }
            ]
          }
        }
        {
          key: "PIP_INSTALLS_LIST"
          value: "mpi4py,pandas,numpy,matplotlib,futures"
          description: "Comma-separated list of Python packages to pip install before the run. Example: 'numpy,scipy,mpi4py' Defaults:'mpi4py,pandas,numpy,scipy'."
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
          }
        }
        {
          key: "MODULE_LOADS_LIST"
          value: "python/3.12.11,opensees,hdf5/1.14.4,pylauncher"
          description: "Comma-separated list of TACC modules to load before the run. Defaults: 'opensees,hdf5/1.14.4' 'python/3.12.11' and 'pylauncher' are included if  GET_TACC_OPENSEESPY=True."
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
          }
        }
        {
          key: "UNZIP_FILES_LIST"
          value: ""
          description: "Comma-separated list of ZIP files in the Input Directory to unzip before the run. Example: 'inputs.zip,gm_files.zip'."
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
        {
          key: "PATH_COPY_IN_LIST"
          value: ""
          description: "Absolute Path (within the Execution System) of folder that will be copied into the job working directory **before** execution.  (Example: '$HOME/FileSet1,$WORK/FileSet2,$SCRATCH/FileSet3/thisFile.at2')"
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
        {
          key: "DELETE_COPIED_IN_ON_EXIT"
          value: "0"
          description: "If set to a true-like value, removes files or directories that were copied into the job working directory via PATH_COPY_IN_LIST after the job completes, preventing temporary inputs from being included in the final archive."
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
        {
          key: "MODULE_LOADS_FILE"
          value: ""
          description: "Name of a file in the Input Directory containing a list of modules to load (newline- or comma-separated). Example: 'modules.txt'."
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
        {
          key: "PIP_INSTALLS_FILE"
          value: ""
          description: "Name of a file in the Input Directory containing a list of Python packages to pip install (newline- or comma-separated). Example: 'requirements.txt'."
          inputMode: "INCLUDE_ON_DEMAND"
          notes: {
            isHidden: False
          }
        }
        {
          key: "ZIP_OUTPUT_SWITCH"
          value: "False"
          description: "If 'True', zip the job output directory into a single archive before Tapis archiving. NOTE: the value must be defined as a string."
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
            enum_values: [
              {
                True: "True: Zip All Output into a file"
              }
              {
                False: "False: No Zipping"
              }
            ]
          }
        }
        {
          key: "PATH_MOVE_OUTPUT"
          value: ""
          description: "Destination path (Absolute and within the Execution System) where outputs will be moved **after** execution. (E.g., '$HOME/OutSet1', '$WORK/OutSet2', '$SCRATCH/OutSet3')"
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
          }
        }
        {
          key: "PRE_JOB_SCRIPT"
          value: ""
          description: "Filename of user-defined PRE-JOB script (or absolute path). This file must reside in the Input Directory. It is run after the system has been configured, but before the main binary. (e.g. prehook.sh,$WORK/.../pre-hook.sh)"
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
          }
        }
        {
          key: "POST_JOB_SCRIPT"
          value: ""
          description: "Filename of user-defined POST-JOB script (or absolute path). This file must reside in the Input Directory. It is run after the the main binary. (e.g. prehook.sh,$WORK/.../pre-hook.sh)"
          inputMode: "INCLUDE_BY_DEFAULT"
          notes: {
            isHidden: False
          }
        }
      ]
      archiveFilter: {
        includeLaunchFiles: True
        includes: []
        excludes: ["designsafe-agnostic-app.zip"]
      }
      logConfig: {
        stdoutFilename: ""
        stderrFilename: ""
      }
    }
  }
  notes: {
    icon: "OpenSees"
    label: "designsafe-agnostic-app"
    helpUrl: ""
    category: "Simulation"
    isInteractive: False
    hideNodeCountAndCoresPerNode: False
  }
}
########################################

Break app schema into detailed parts and review them for possible input arguments#

# Convert TapisResults Objects to dictionaries
app_MetaData = thisAppData_MP.__dict__
# Review the keys to see if there is any input we are interested in. 
# You can view the full schema above to see the values
# print('app_MetaData.keys',app_MetaData.keys())
dictKeys = []
TapisResultKeys = []
listKeys = []
print('*** MAIN INPUT***')
for thisKey,thisValue in app_MetaData.items():
    if isinstance(thisValue, dict):
        dictKeys.append(thisKey)
    if isinstance(thisValue, TapisResult):
        TapisResultKeys.append(thisKey)
    elif isinstance(thisValue, list):
        listKeys.append(thisKey)
    else:
        print(f'{thisKey} = {thisValue}')
        
print('*\n--- Nested Objects---')
print('dict-type input keys',dictKeys)
print('list-type input keys',listKeys)
print('TapisResult-type input keys',TapisResultKeys)
*** MAIN INPUT***
sharedAppCtx = silvia
isPublic = True
tenant = designsafe
id = designsafe-agnostic-app
version = 1.2.0
description = Agnostic Tapis App for General Python Execution as well as OpenSees, OpenSeesMP, OpenSeesSP, OpenSeesPy
owner = silvia
enabled = True
versionEnabled = True
locked = False
runtime = ZIP
runtimeVersion = None
runtimeOptions = None
containerImage = //work2/05072/silvia/stampede3/apps/designsafe-agnostic-app/1.2.0/designsafe-agnostic-app.zip
jobType = BATCH
maxJobs = 2147483647
maxJobsPerUser = 2147483647
strictFileInputs = False
uuid = e3854c4d-470b-46d3-b9b6-eec487f8f23e
deleted = False
created = 2026-02-05T16:13:21.989088Z
updated = 2026-02-05T16:13:21.989088Z
*
--- Nested Objects---
dict-type input keys []
list-type input keys ['sharedWithUsers', 'tags']
TapisResult-type input keys ['jobAttributes', 'notes']

Check App Basics#

A few things to check here:

  1. app name and version are what you want

  2. isPublic = True (you can use the app)

  3. if isPublic = False, make sure owner = usename

  4. enabled = True (app is usable)

  5. deleted = False (it exists)

  6. read the description

for thisKey in ['id','version','description','isPublic','enabled']:
    print(f'{thisKey}: {app_MetaData[thisKey]}')
id: designsafe-agnostic-app
version: 1.2.0
description: Agnostic Tapis App for General Python Execution as well as OpenSees, OpenSeesMP, OpenSeesSP, OpenSeesPy
isPublic: True
enabled: True

Look at the different types of arguments#

List#

print(f'***** list-type INPUT ****')
if len(listKeys)>0:
    for thisKey in listKeys:
        thisList = app_MetaData[thisKey]
        print(f'{thisKey} : {thisList}')
else: 
    print('-none')
***** list-type INPUT ****
sharedWithUsers : []
tags : ['portalName: DesignSafe', 'portalName: CEP']
# Nothing related to input in the lists.

Dictionaries#

print(f'***** dict-type INPUT ****')
if len(dictKeys)>0:
    for thisKey in dictKeys:
        thisDict = app_MetaData[thisKey]
        print(f'{thisKey} : {thisDict}')
else: 
    print('there are no dictionaries within jobAttributes, as expected, since Tapis works with TapisResults objects instead')
***** dict-type INPUT ****
there are no dictionaries within jobAttributes, as expected, since Tapis works with TapisResults objects instead

TapisResult#

print(f'***** TapisResult-type INPUT ****')
TR_dictKeys = {}
TR_TapisResultKeys = {}
TR_listKeys = {}
for hereKey in TapisResultKeys:
    print(f'\n*** {hereKey} ***')
    thisTapisResult = app_MetaData[hereKey]
    thisTapisResult_dict = thisTapisResult.__dict__
    TR_dictKeys[hereKey] = []
    TR_TapisResultKeys[hereKey] = []
    TR_listKeys[hereKey] = []
    print(f'\n  --- Configuration Arguments ---')
    for thisKey,thisValue in thisTapisResult_dict.items():
        if isinstance(thisValue, dict):
            TR_dictKeys[hereKey].append(thisKey)
        if isinstance(thisValue, TapisResult):
            TR_TapisResultKeys[hereKey].append(thisKey)
        elif isinstance(thisValue, list):
            TR_listKeys[hereKey].append(thisKey)
        else:
            print(f'  {thisKey} = {thisValue}')
    print(f'\n  --- {hereKey} -- Nested Objects---')
    print(f'   dict-type input keys',TR_dictKeys[hereKey])
    print(f'   list-type input keys',TR_listKeys[hereKey])
    print(f'   TapisResult-type input keys',TR_TapisResultKeys[hereKey])
    print('')
***** TapisResult-type INPUT ****

*** jobAttributes ***

  --- Configuration Arguments ---
  description = None
  dynamicExecSystem = False
  execSystemConstraints = None
  execSystemId = stampede3
  execSystemExecDir = ${JobWorkingDir}
  execSystemInputDir = ${JobWorkingDir}
  execSystemOutputDir = ${JobWorkingDir}
  dtnSystemInputDir = !tapis_not_set
  dtnSystemOutputDir = !tapis_not_set
  execSystemLogicalQueue = skx-dev
  archiveSystemId = designsafe.storage.default
  archiveSystemDir = silvia/tapis-jobs-archive/${JobCreateDate}/${JobName}-${JobUUID}
  archiveOnAppError = True
  archiveMode = None
  isMpi = False
  mpiCmd = None
  cmdPrefix = None
  nodeCount = 1
  coresPerNode = 48
  memoryMB = 192000
  maxMinutes = 120

  --- jobAttributes -- Nested Objects---
   dict-type input keys []
   list-type input keys ['fileInputs', 'fileInputArrays', 'subscriptions', 'tags']
   TapisResult-type input keys ['parameterSet']


*** notes ***

  --- Configuration Arguments ---
  icon = OpenSees
  label = designsafe-agnostic-app
  helpUrl = 
  category = Simulation
  isInteractive = False
  hideNodeCountAndCoresPerNode = False

  --- notes -- Nested Objects---
   dict-type input keys []
   list-type input keys []
   TapisResult-type input keys []

We see that we have jobAttribues and notes

  • notes this is informational content for the web portal

  • jobAttributes is the App-Specific Job input. Within this json object we have the following categories of job input:

    • configuration – these are the direct input, such as execSystemId – where the job will be run, such as stampede3

    • fileInputs – this is a list object

    • fileInputArrays – this is a list object

    • subscriptions – this is a list object

    • parameterSet – this is a json object

notes {}#

notes is just informational.


App User Input#

Initialize#

# initalize
TapisInput = {}
TapisInput["name"] = f'TestJob-${appId}' # not the job name, used just for bookkeeping

jobAttributes#

This is the content we will be submitting to the app.

jobAttributes has valuable input arguments as well as nested ones.

  1. We need to initialize this json object in our TapisInput.

  2. Let’s look at the high-level input

  3. Let’s look at the nested content

  1. Initialize TapisInput[‘jobAttributes’]

TapisInput['jobAttributes'] = {}
# start this input with app id and version to jobAttributes since that is what we send to tapis
TapisInput['jobAttributes']["name"] = f'My first Tapis Job on {appId}'
TapisInput['jobAttributes']["appId"] = appId
TapisInput['jobAttributes']["appVersion"] = appVersion
  1. Look at high-level variables to see if there is anything of value

  • This is where you find information about where the job is run (execSystemId)

  • In HPC applications this is where you define the SLURM input on queues and nodes, etc

TapisInput['jobAttributes']['execSystemId'] = "stampede3"; # we don't really need to specify this because stampede3 is already the default value
# slurmm-job input
TapisInput['jobAttributes']['execSystemLogicalQueue'] = 'skx-dev'
TapisInput['jobAttributes']['nodeCount'] = 1
TapisInput['jobAttributes']['coresPerNode'] = 16
TapisInput['jobAttributes']['maxMinutes'] = 7

3. Let’s dig into the first level – app.jobAttributes#

# Let's extract and study jobAttributes
myKey = 'jobAttributes'
app_jobAttributes = app_MetaData[myKey].__dict__
# we have already extracted the contents of this dict

print(f'*****  {myKey} dict-type Input ****')
if len(TR_dictKeys[myKey])>0:
    for thisKey in TR_dictKeys[myKey]:
        thisDict = app_jobAttributes[thisKey]
        print(f'{myKey}.{thisKey} : {thisDict}')
else:
    print('there are no dictionaries within jobAttributes, as expected, since Tapis works with TapisResults objects instead')
*****  jobAttributes dict-type Input ****
there are no dictionaries within jobAttributes, as expected, since Tapis works with TapisResults objects instead

print(f'*****  {myKey} list-type input****')
if len(TR_listKeys[myKey])>0:
    for thisKey in TR_listKeys[myKey]:
        thisList = app_jobAttributes[thisKey]
        if len(thisList)>0:
            print(f'  {thisKey} = ')
            print('    [')
            for thisValue in thisList:
                if isinstance(thisValue, TapisResult):
                    thisValue = thisValue.__dict__
                print(f'      {thisValue}')
            print('    ]')
        else:
            print(f'  {thisKey}: {thisList}')
else:
    print('none')
*****  jobAttributes list-type input****
  fileInputs = 
    [
      {'name': 'Input Directory', 'description': 'Directory containing the main script and any supporting files (models, data, etc.). (Example: tapis://designsafe.storage.community/app_examples/opensees/OpenSeesPy)', 'inputMode': 'REQUIRED', 'autoMountLocal': True, 'envKey': 'inputDirectory', 'notes': 
isHidden: False
selectionMode: directory, 'sourceUrl': None, 'targetPath': 'inputDirectory'}
    ]
  fileInputArrays: []
  subscriptions: []
  tags: []

fileInputs#

Here we find that jobAttributes.fileInputs is a REQUIRED input.

In this case we need to get the tapisURI for our input directory.

Storage SystemTapis & Tapis Base Path in URI format#

this is the very first part of your path, just above your home folder.

Options:

  • CommunityData

  • Published

The following options are user or project-dependent, and require unique path input.

The following option requires additional user-dependent input:

  • MyData

The following option requires additional user- and system- dependent input:

  • Work

The following option requires additional project-dependent input:

  • MyProjects

You can obtain a dependent tapis-URI path by performing the first step of submitting an OpenSeesMP job at the app portal: https://www.designsafe-ci.org/workspace/opensees-mp-s3

storage_system = 'MyData' # options: Community,MyData,Published,MyProjects,Work/stampede3,Work/frontera,Work/ls6
storage_system_baseURL = OpsUtils.get_user_path_tapis_uri(t,storage_system)

print('storage_system_baseURL:',storage_system_baseURL)
found paths file: /home/jupyter/MyData/.tapis_user_paths.json
storage_system_baseURL: tapis://designsafe.storage.default/silvia
input_folder = '_ToCommunityData/OpenSees/TrainingMaterial/training-OpenSees-on-DesignSafe/Examples_OpenSees/BasicExamples'
sourceUrl = f'{storage_system_baseURL}/{input_folder}'
print('sourceUrl',sourceUrl)
sourceUrl tapis://designsafe.storage.default/silvia/_ToCommunityData/OpenSees/TrainingMaterial/training-OpenSees-on-DesignSafe/Examples_OpenSees/BasicExamples
TapisInput['jobAttributes']['fileInputs'] = [{'name': 'Input Directory','sourceUrl':sourceUrl}]; # notice that it is a list!

print(f'***** {myKey} TapisResult-type input ****')
if len(TR_TapisResultKeys[myKey])>0:
    for hereKey in TR_TapisResultKeys[myKey]:
        print(f'\n*** {hereKey} ***')
        thisTapisResult = app_jobAttributes[hereKey]
        thisTapisResult_dict = thisTapisResult.__dict__
        TR_dictKeys[hereKey] = []
        TR_TapisResultKeys[hereKey] = []
        TR_listKeys[hereKey] = []
        for thisKey,thisValue in thisTapisResult_dict.items():
            if isinstance(thisValue, dict):
                TR_dictKeys[hereKey].append(thisKey)
            if isinstance(thisValue, TapisResult):
                TR_TapisResultKeys[hereKey].append(thisKey)
            elif isinstance(thisValue, list):
                TR_listKeys[hereKey].append(thisKey)
            else:
                print(f'  {myKey}.{thisKey} = {thisValue}')
        print(f'\n  --- {hereKey} -- Nested Objects---')
        print(f'   {myKey}.{hereKey} dict-type input keys',TR_dictKeys[hereKey])
        print(f'   {myKey}.{hereKey} list-type input keys',TR_listKeys[hereKey])
        print(f'   {myKey}.{hereKey} TapisResult-type input keys',TR_TapisResultKeys[hereKey])
        print('')
else:
    print('-none')
***** jobAttributes TapisResult-type input ****

*** parameterSet ***

  --- parameterSet -- Nested Objects---
   jobAttributes.parameterSet dict-type input keys []
   jobAttributes.parameterSet list-type input keys ['appArgs', 'containerArgs', 'schedulerOptions', 'envVariables']
   jobAttributes.parameterSet TapisResult-type input keys ['archiveFilter', 'logConfig']

app.jobAttributes.parameterSet is the interesting one#

# Let's extract and study jobAttributes
myKey = 'parameterSet'
app_parameterSet = app_jobAttributes[myKey].__dict__
# we have already extracted the contents of this dict
## parameterSet is a dictionary (TapisResult):
TapisInput['jobAttributes'][myKey] = {}

print(f'*****  {myKey} dict-type Input ****')
if len(TR_dictKeys[myKey])>0:
    for thisKey in TR_dictKeys[myKey]:
        thisDict = app_parameterSet[thisKey]
        print(f'{myKey}.{thisKey} : {thisDict}')
else:
    print('there are no dictionaries within app_parameterSet, as expected, since Tapis works with TapisResults objects instead')
*****  parameterSet dict-type Input ****
there are no dictionaries within app_parameterSet, as expected, since Tapis works with TapisResults objects instead

print(f'*****  {myKey} list-type input****')
if len(TR_listKeys[myKey])>0:
    for thisKey in TR_listKeys[myKey]:
        thisList = app_parameterSet[thisKey]
        if len(thisList)>0:
            print(f'  {thisKey} = ')
            print('    [')
            for thisValue in thisList:
                if isinstance(thisValue, TapisResult):
                    thisValue = thisValue.__dict__
                print(f'      {str(thisValue)}')
            print('    ]')
        else:
            print(f'  {thisKey}: {thisList}')
else:
    print('none')
*****  parameterSet list-type input****
  appArgs = 
    [
      {'arg': 'python3', 'name': 'Main Program', 'description': "Binary executable to run. (e.g., OpenSees, OpenSeesMP, OpenSeesSP, python3 -- OpenSeesPy: use python3).    The executable must be available in the job's execution system. Some executables require you to load specific modules.", 'inputMode': 'REQUIRED', 'notes': 
enum_values: [
OpenSees: OpenSees, 
OpenSeesMP: OpenSeesMP, 
OpenSeesSP: OpenSeesSP, 
python3: Python]
isHidden: False}
      {'arg': None, 'name': 'Main Script', 'description': "Filename (no path) of the input script passed to the executable (Example: Ex1a.Canti2D.Push.mpi4py.tacc.py). This file must reside in the Input Directory.  Note: This App uses TACC-Compiled OpenSeesPy: use 'import opensees' or 'import opensees as ops' in your script.", 'inputMode': 'REQUIRED', 'notes': 
inputType: fileInput
isHidden: False}
      {'arg': 'True', 'name': 'UseMPI', 'description': 'Flag indicating whether the application should launch the main program with an MPI parallel-execution command (ibrun). **True**: enable distributed-memory parallelism, allowing multi-core or multi-node execution. (Suitable for OpenSeesMP / OpenSeesSP / Python with mpi4py (OpenSeesPy)). **False**: execution stays on one node. (Suitable for OpenSees, Python, or Python with concurrent.futures for one-node parallelism.)', 'inputMode': 'REQUIRED', 'notes': 
enum_values: [
True: True — Enable MPI mode -- Use multi-node or multi-core parallelism., 
False: False — No MPI -- Use single-node process.]
isHidden: False}
      {'arg': None, 'name': 'CommandLine Arguments', 'description': "Optional command-line arguments appended after Main Script (e.g., '--npts 2000 --dir X' or any format consistent with how your input script parses them).", 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
    ]
  containerArgs: []
  schedulerOptions = 
    [
      {'arg': '--tapis-profile tacc-no-modules', 'name': 'TACC Scheduler Profile', 'description': 'Scheduler profile (e.g., tacc-no-modules) -- the app loads the modules you specify.', 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
isHidden: False}
      {'arg': None, 'name': 'TACC Reservation', 'description': 'If you have a TACC reservation, enter the reservation string here.', 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
    ]
  envVariables = 
    [
      {'key': 'GET_TACC_OPENSEESPY', 'value': 'True', 'description': "If 'True', use the TACC-compiled OpenSeesPy (not the PyPI wheel). In your script, import OpenSeesPy using 'import opensees' or 'import opensees as ops'.", 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
enum_values: [
True: True: Copy TACC-Compiled OpenSeesPy, 
False: False: no TACC-Compiled OpenSeesPy]
isHidden: False}
      {'key': 'PIP_INSTALLS_LIST', 'value': 'mpi4py,pandas,numpy,matplotlib,futures', 'description': "Comma-separated list of Python packages to pip install before the run. Example: 'numpy,scipy,mpi4py' Defaults:'mpi4py,pandas,numpy,scipy'.", 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
isHidden: False}
      {'key': 'MODULE_LOADS_LIST', 'value': 'python/3.12.11,opensees,hdf5/1.14.4,pylauncher', 'description': "Comma-separated list of TACC modules to load before the run. Defaults: 'opensees,hdf5/1.14.4' 'python/3.12.11' and 'pylauncher' are included if  GET_TACC_OPENSEESPY=True.", 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
isHidden: False}
      {'key': 'UNZIP_FILES_LIST', 'value': '', 'description': "Comma-separated list of ZIP files in the Input Directory to unzip before the run. Example: 'inputs.zip,gm_files.zip'.", 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
      {'key': 'PATH_COPY_IN_LIST', 'value': '', 'description': "Absolute Path (within the Execution System) of folder that will be copied into the job working directory **before** execution.  (Example: '$HOME/FileSet1,$WORK/FileSet2,$SCRATCH/FileSet3/thisFile.at2')", 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
      {'key': 'DELETE_COPIED_IN_ON_EXIT', 'value': '0', 'description': 'If set to a true-like value, removes files or directories that were copied into the job working directory via PATH_COPY_IN_LIST after the job completes, preventing temporary inputs from being included in the final archive.', 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
      {'key': 'MODULE_LOADS_FILE', 'value': '', 'description': "Name of a file in the Input Directory containing a list of modules to load (newline- or comma-separated). Example: 'modules.txt'.", 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
      {'key': 'PIP_INSTALLS_FILE', 'value': '', 'description': "Name of a file in the Input Directory containing a list of Python packages to pip install (newline- or comma-separated). Example: 'requirements.txt'.", 'inputMode': 'INCLUDE_ON_DEMAND', 'notes': 
isHidden: False}
      {'key': 'ZIP_OUTPUT_SWITCH', 'value': 'False', 'description': "If 'True', zip the job output directory into a single archive before Tapis archiving. NOTE: the value must be defined as a string.", 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
enum_values: [
True: True: Zip All Output into a file, 
False: False: No Zipping]
isHidden: False}
      {'key': 'PATH_MOVE_OUTPUT', 'value': '', 'description': "Destination path (Absolute and within the Execution System) where outputs will be moved **after** execution. (E.g., '$HOME/OutSet1', '$WORK/OutSet2', '$SCRATCH/OutSet3')", 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
isHidden: False}
      {'key': 'PRE_JOB_SCRIPT', 'value': '', 'description': 'Filename of user-defined PRE-JOB script (or absolute path). This file must reside in the Input Directory. It is run after the system has been configured, but before the main binary. (e.g. prehook.sh,$WORK/.../pre-hook.sh)', 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
isHidden: False}
      {'key': 'POST_JOB_SCRIPT', 'value': '', 'description': 'Filename of user-defined POST-JOB script (or absolute path). This file must reside in the Input Directory. It is run after the the main binary. (e.g. prehook.sh,$WORK/.../pre-hook.sh)', 'inputMode': 'INCLUDE_BY_DEFAULT', 'notes': 
isHidden: False}
    ]

While the opensees-mp-s3 and opensees-sp-s3 apps use appArgs, this app uses envVariables for the input.

It looks like enum_values in notes is a list for a pull-down menu, and, interestingly, it is missing OpenSeesMP – from the app definition in github, they Removed because OpenSeesMP is unable to use multiple cores, essentially making it SP

print('review each of these items:', TR_listKeys[myKey])
review each of these items: ['appArgs', 'containerArgs', 'schedulerOptions', 'envVariables']

app.jobAttributes.parameterSet.appArgs#

thisKey = 'appArgs'
TapisInput['jobAttributes']['parameterSet'][thisKey] = [] # it's a list, initialize as a list!
print(f'** app.jobAttributes.parameterSet.{thisKey} **')
if len(app_parameterSet[thisKey])>0:
    for thisItem in app_parameterSet[thisKey]:
        print(thisItem)
else:
    print('.none.')
** app.jobAttributes.parameterSet.appArgs **

arg: python3
description: Binary executable to run. (e.g., OpenSees, OpenSeesMP, OpenSeesSP, python3 -- OpenSeesPy: use python3).    The executable must be available in the job's execution system. Some executables require you to load specific modules.
inputMode: REQUIRED
name: Main Program
notes: 
enum_values: [
OpenSees: OpenSees, 
OpenSeesMP: OpenSeesMP, 
OpenSeesSP: OpenSeesSP, 
python3: Python]
isHidden: False

arg: None
description: Filename (no path) of the input script passed to the executable (Example: Ex1a.Canti2D.Push.mpi4py.tacc.py). This file must reside in the Input Directory.  Note: This App uses TACC-Compiled OpenSeesPy: use 'import opensees' or 'import opensees as ops' in your script.
inputMode: REQUIRED
name: Main Script
notes: 
inputType: fileInput
isHidden: False

arg: True
description: Flag indicating whether the application should launch the main program with an MPI parallel-execution command (ibrun). **True**: enable distributed-memory parallelism, allowing multi-core or multi-node execution. (Suitable for OpenSeesMP / OpenSeesSP / Python with mpi4py (OpenSeesPy)). **False**: execution stays on one node. (Suitable for OpenSees, Python, or Python with concurrent.futures for one-node parallelism.)
inputMode: REQUIRED
name: UseMPI
notes: 
enum_values: [
True: True — Enable MPI mode -- Use multi-node or multi-core parallelism., 
False: False — No MPI -- Use single-node process.]
isHidden: False

arg: None
description: Optional command-line arguments appended after Main Script (e.g., '--npts 2000 --dir X' or any format consistent with how your input script parses them).
inputMode: INCLUDE_ON_DEMAND
name: CommandLine Arguments
notes: 
isHidden: False

Nothing to input here!

app.jobAttributes.parameterSet.containerArgs#

thisKey = 'containerArgs'
TapisInput['jobAttributes']['parameterSet'][thisKey] = [] # it's a list, initialize as a list!
print(f'** app.jobAttributes.parameterSet.{thisKey} **')
if len(app_parameterSet[thisKey])>0:
    for thisItem in app_parameterSet[thisKey]:
        print(thisItem)
else:
    print('.none.')
** app.jobAttributes.parameterSet.containerArgs **
.none.
# do nothing for this app.

app.jobAttributes.parameterSet.schedulerOptions#

thisKey = 'schedulerOptions'
TapisInput['jobAttributes']['parameterSet'][thisKey] = [] # it's a list, initialize as a list!
print(f'** app.jobAttributes.parameterSet.{thisKey} **')
if len(app_parameterSet[thisKey])>0:
    for thisItem in app_parameterSet[thisKey]:
        print(thisItem)
else:
    print('.none.')
** app.jobAttributes.parameterSet.schedulerOptions **

arg: --tapis-profile tacc-no-modules
description: Scheduler profile (e.g., tacc-no-modules) -- the app loads the modules you specify.
inputMode: INCLUDE_BY_DEFAULT
name: TACC Scheduler Profile
notes: 
isHidden: False

arg: None
description: If you have a TACC reservation, enter the reservation string here.
inputMode: INCLUDE_ON_DEMAND
name: TACC Reservation
notes: 
isHidden: False

There is no scheduler nor allocation used in OpenSees-Express.

NO NEED to add allocation to the scheduler option – not shown in the app schema#

# user_allocation = '-A DS-HPC1'; # you get this code from your allocation dashboard
# TapisInput['jobAttributes']['parameterSet']['schedulerOptions'].append({'name': 'TACC Allocation', 'arg': user_allocation})

app.jobAttributes.parameterSet.envVariables#

thisKey = 'envVariables'
TapisInput['jobAttributes']['parameterSet'][thisKey] = [] # it's a list, initialize as a list!
print(f'** app.jobAttributes.parameterSet.{thisKey} **')
if len(app_parameterSet[thisKey])>0:
    for thisItem in app_parameterSet[thisKey]:
        print(thisItem)
else:
    print('.none.')
** app.jobAttributes.parameterSet.envVariables **

description: If 'True', use the TACC-compiled OpenSeesPy (not the PyPI wheel). In your script, import OpenSeesPy using 'import opensees' or 'import opensees as ops'.
inputMode: INCLUDE_BY_DEFAULT
key: GET_TACC_OPENSEESPY
notes: 
enum_values: [
True: True: Copy TACC-Compiled OpenSeesPy, 
False: False: no TACC-Compiled OpenSeesPy]
isHidden: False
value: True

description: Comma-separated list of Python packages to pip install before the run. Example: 'numpy,scipy,mpi4py' Defaults:'mpi4py,pandas,numpy,scipy'.
inputMode: INCLUDE_BY_DEFAULT
key: PIP_INSTALLS_LIST
notes: 
isHidden: False
value: mpi4py,pandas,numpy,matplotlib,futures

description: Comma-separated list of TACC modules to load before the run. Defaults: 'opensees,hdf5/1.14.4' 'python/3.12.11' and 'pylauncher' are included if  GET_TACC_OPENSEESPY=True.
inputMode: INCLUDE_BY_DEFAULT
key: MODULE_LOADS_LIST
notes: 
isHidden: False
value: python/3.12.11,opensees,hdf5/1.14.4,pylauncher

description: Comma-separated list of ZIP files in the Input Directory to unzip before the run. Example: 'inputs.zip,gm_files.zip'.
inputMode: INCLUDE_ON_DEMAND
key: UNZIP_FILES_LIST
notes: 
isHidden: False
value: 

description: Absolute Path (within the Execution System) of folder that will be copied into the job working directory **before** execution.  (Example: '$HOME/FileSet1,$WORK/FileSet2,$SCRATCH/FileSet3/thisFile.at2')
inputMode: INCLUDE_ON_DEMAND
key: PATH_COPY_IN_LIST
notes: 
isHidden: False
value: 

description: If set to a true-like value, removes files or directories that were copied into the job working directory via PATH_COPY_IN_LIST after the job completes, preventing temporary inputs from being included in the final archive.
inputMode: INCLUDE_ON_DEMAND
key: DELETE_COPIED_IN_ON_EXIT
notes: 
isHidden: False
value: 0

description: Name of a file in the Input Directory containing a list of modules to load (newline- or comma-separated). Example: 'modules.txt'.
inputMode: INCLUDE_ON_DEMAND
key: MODULE_LOADS_FILE
notes: 
isHidden: False
value: 

description: Name of a file in the Input Directory containing a list of Python packages to pip install (newline- or comma-separated). Example: 'requirements.txt'.
inputMode: INCLUDE_ON_DEMAND
key: PIP_INSTALLS_FILE
notes: 
isHidden: False
value: 

description: If 'True', zip the job output directory into a single archive before Tapis archiving. NOTE: the value must be defined as a string.
inputMode: INCLUDE_BY_DEFAULT
key: ZIP_OUTPUT_SWITCH
notes: 
enum_values: [
True: True: Zip All Output into a file, 
False: False: No Zipping]
isHidden: False
value: False

description: Destination path (Absolute and within the Execution System) where outputs will be moved **after** execution. (E.g., '$HOME/OutSet1', '$WORK/OutSet2', '$SCRATCH/OutSet3')
inputMode: INCLUDE_BY_DEFAULT
key: PATH_MOVE_OUTPUT
notes: 
isHidden: False
value: 

description: Filename of user-defined PRE-JOB script (or absolute path). This file must reside in the Input Directory. It is run after the system has been configured, but before the main binary. (e.g. prehook.sh,$WORK/.../pre-hook.sh)
inputMode: INCLUDE_BY_DEFAULT
key: PRE_JOB_SCRIPT
notes: 
isHidden: False
value: 

description: Filename of user-defined POST-JOB script (or absolute path). This file must reside in the Input Directory. It is run after the the main binary. (e.g. prehook.sh,$WORK/.../pre-hook.sh)
inputMode: INCLUDE_BY_DEFAULT
key: POST_JOB_SCRIPT
notes: 
isHidden: False
value: 

YES this app does have input here, we need to specify the execution program and the tcl script#

Note that the names of the labels for the keys here are key and value , not name and arg, and the keys are different from opensees-mp-s3

Make sure your tcl script is for a sequential analysis.

TapisInput['jobAttributes']['parameterSet']['envVariables'].append({"key": "mainProgram", "value": 'OpenSees'})
TapisInput['jobAttributes']['parameterSet']['envVariables'].append({"key": "tclScript", "value": 'Ex1a.Canti2D.Push.tcl'})

print(f'***** {myKey} TapisResult-type input ****')
if len(TR_TapisResultKeys[myKey])>0:
    for hereKey in TR_TapisResultKeys[myKey]:
        print(f'\n*** {hereKey} ***')
        thisTapisResult = app_parameterSet[hereKey]
        thisTapisResult_dict = thisTapisResult.__dict__
        TR_dictKeys[hereKey] = []
        TR_TapisResultKeys[hereKey] = []
        TR_listKeys[hereKey] = []
        for thisKey,thisValue in thisTapisResult_dict.items():
            if isinstance(thisValue, dict):
                TR_dictKeys[hereKey].append(thisKey)
            if isinstance(thisValue, TapisResult):
                TR_TapisResultKeys[hereKey].append(thisKey)
            elif isinstance(thisValue, list):
                TR_listKeys[hereKey].append(thisKey)
            else:
                print(f'  {myKey}.{thisKey} = {thisValue}')
        print(f'\n  --- {hereKey} -- Nested Objects---')
        print(f'   {myKey}.{hereKey} dict-type input keys',TR_dictKeys[hereKey])
        print(f'   {myKey}.{hereKey} list-type input keys',TR_listKeys[hereKey])
        print(f'   {myKey}.{hereKey} TapisResult-type input keys',TR_TapisResultKeys[hereKey])
        print('')
else:
    print('-none')
***** parameterSet TapisResult-type input ****

*** archiveFilter ***
  parameterSet.includeLaunchFiles = True

  --- archiveFilter -- Nested Objects---
   parameterSet.archiveFilter dict-type input keys []
   parameterSet.archiveFilter list-type input keys ['includes', 'excludes']
   parameterSet.archiveFilter TapisResult-type input keys []


*** logConfig ***
  parameterSet.stdoutFilename = 
  parameterSet.stderrFilename = 

  --- logConfig -- Nested Objects---
   parameterSet.logConfig dict-type input keys []
   parameterSet.logConfig list-type input keys []
   parameterSet.logConfig TapisResult-type input keys []
# nothing of interest to us here...

We are done with our tapis-job input#

print('TapisInput')
display(TapisInput)
TapisInput
{'name': 'TestJob-$designsafe-agnostic-app',
 'jobAttributes': {'name': 'My first Tapis Job on designsafe-agnostic-app',
  'appId': 'designsafe-agnostic-app',
  'appVersion': 'latest',
  'execSystemId': 'stampede3',
  'execSystemLogicalQueue': 'skx-dev',
  'nodeCount': 1,
  'coresPerNode': 16,
  'maxMinutes': 7,
  'fileInputs': [{'name': 'Input Directory',
    'sourceUrl': 'tapis://designsafe.storage.default/silvia/_ToCommunityData/OpenSees/TrainingMaterial/training-OpenSees-on-DesignSafe/Examples_OpenSees/BasicExamples'}],
  'parameterSet': {'appArgs': [],
   'containerArgs': [],
   'schedulerOptions': [],
   'envVariables': [{'key': 'mainProgram', 'value': 'OpenSees'},
    {'key': 'tclScript', 'value': 'Ex1a.Canti2D.Push.tcl'}]}}}
print('TapisInput.jobAttributes')
display(TapisInput['jobAttributes'])
TapisInput.jobAttributes
{'name': 'My first Tapis Job on designsafe-agnostic-app',
 'appId': 'designsafe-agnostic-app',
 'appVersion': 'latest',
 'execSystemId': 'stampede3',
 'execSystemLogicalQueue': 'skx-dev',
 'nodeCount': 1,
 'coresPerNode': 16,
 'maxMinutes': 7,
 'fileInputs': [{'name': 'Input Directory',
   'sourceUrl': 'tapis://designsafe.storage.default/silvia/_ToCommunityData/OpenSees/TrainingMaterial/training-OpenSees-on-DesignSafe/Examples_OpenSees/BasicExamples'}],
 'parameterSet': {'appArgs': [],
  'containerArgs': [],
  'schedulerOptions': [],
  'envVariables': [{'key': 'mainProgram', 'value': 'OpenSees'},
   {'key': 'tclScript', 'value': 'Ex1a.Canti2D.Push.tcl'}]}}
if submitJob:
    submitted_job = t.jobs.submitJob(**TapisInput['jobAttributes'])
if submitJob:
    print(submitted_job)

Once you have no errors you are done submitting the job.#

go to the job-status page on the web portal to monitor the your job.#

https://www.designsafe-ci.org/workspace/history

You will, likely, have to debug your OpenSees script…..

NOTE ONCE THE JOB HAS COMPLETED, go to view Output and pay attention to the location (path) of the output, it may not be in MyData, as expected.

print('done')
done