From 8c18c72bb368b5902b301835b90fcfbedc254b24 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Thu, 21 May 2020 12:22:24 -0700 Subject: [PATCH 01/15] Initial pgen docs with itertools example --- docs/source/index.rst | 1 + docs/source/parameters.rst | 109 +++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 docs/source/parameters.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index e84034e05..b20c1ccc9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ Welcome to Maestro Workflow Conductor Documentation quick_start hello_world lulesh_breakdown + parameters maestro_core modules diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst new file mode 100644 index 000000000..2e03bc4bf --- /dev/null +++ b/docs/source/parameters.rst @@ -0,0 +1,109 @@ +Parameters +========== + +Parameter Generator (pgen) +************************** + +Maestro's Parameter Generator (pgen) supports setting up more flexible and complex parameter generation. Maestro's pgen is a user supplied python file that contains the parameter generation logic, overriding the global.parameters block in the yaml specification file. To run a Maestro study using a parameter generator just pass in the pgen file to Maestro on the command line when launching the study: + +.. code-block:: bash + + $ maestro run study.yaml --pgen pgen.py + +The minimum requirements for making a valid pgen file is to make a function called ``get_custom_generator`` which returns a Maestro :py:class:`~maestrowf.datastructures.core.ParameterGenerator` object as demonstrated in the simple example below: + +.. code-block:: python + :linenos: + + from maestrowf.datastructures.core import ParameterGenerator + + def get_custom_generator(env, **kwargs): + p_gen = ParameterGenerator() + params = { + "COUNT": { + "values": [i for i in range(1, 10)], + "label": "COUNT.%%" + }, + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen + + +The object simply builds the same nested key:value pairs seen in the global.parameters block available in the yaml specification. + +For this simple example above, this may not offer compelling advantages over writing out the flattened list in the yaml specification directly. This programmatic approach becomes preferable when expanding studies to use hundreds of parameters and parameter values or requiring non-trivial parameter value distributions. The following examples will demonstrate these scenarios using both standard python library tools and additional 3rd party packages from the larger python ecosystem. + +EXAMPLES: + Simple for loops and Itertools for pure python work (side by side with lulesh example) + products, permutations and combinations + pargs for dynamic generators + + section on 3rd party tools: note on virtualenvironments to make it work + numpy/scipy for chebyshev distribution: use extra function in the pgen file + find another sampling algorithm: latin hypercube, or something else from scikit-learn? what about stats models? + +What about adding reference of env block in pgen? (not modifying, just referencing) + + +First, lets use the excellent built in package itertools to generate the parameters in the lulesh example specification: + +.. code-block:: python + :name: itertools_pgen.py + :caption: itertools.pgen.py + :linenos: + + from maestrowf.datastructures.core import ParameterGenerator + import itertools as iter + + def get_custom_generator(env, **kwargs): + p_gen = ParameterGenerator() + + sizes = (10, 20, 30) + iterations = (10, 20, 30) + + size_values = [] + iteration_values = [] + trial_values = [] + + for trial, param_combo in enumerate(iter.product(sizes, iterations)): + size_values.append(param_combo[0]) + iteration_values.append(param_combo[1]) + trial_values.append(trial) + + params = { + "TRIAL": { + "values": trial_values, + "label": "TRIAL.%%" + }, + "SIZE": { + "values": size_values, + "label": "SIZE.%%" + }, + "ITER": { + "values": iteration_values, + "label": "ITER.%%" + }, + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen + +This results in the following set of parameters, matching the lulesh sample workflow: + +.. table:: Sample parameters from itertools_pgen.py + + =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== + Parameter Values + ----------- -------------------------------------------- + TRIAL 0 1 2 3 4 5 6 7 8 + ----------- ---- ---- ---- ---- ---- ---- ---- ---- ---- + SIZE 10 10 10 20 20 20 30 30 30 + ----------- ---- ---- ---- ---- ---- ---- ---- ---- ---- + ITER 10 20 30 10 20 30 10 20 30 + =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== + From 3e87ec6f82c9f3fbc6576b5e75923c801aad8948 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Thu, 21 May 2020 12:43:40 -0700 Subject: [PATCH 02/15] Add pargs example --- docs/source/parameters.rst | 74 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index 2e03bc4bf..188dac5d4 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -52,7 +52,7 @@ First, lets use the excellent built in package itertools to generate the paramet .. code-block:: python :name: itertools_pgen.py - :caption: itertools.pgen.py + :caption: itertools_pgen.py :linenos: from maestrowf.datastructures.core import ParameterGenerator @@ -106,4 +106,74 @@ This results in the following set of parameters, matching the lulesh sample work ----------- ---- ---- ---- ---- ---- ---- ---- ---- ---- ITER 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== - + +There is an additional pgen feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the itertools_pgen.py file to change that. Maestro supports passing arguments to these generator functions on the command line: + + +.. code-block:: bash + + $ maestro run study.yaml --pgen pgen.py --parg "SIZE_MIN:10" --parg "SIZE_STEP: 10" --parg "NUM_SIZES:4" + +Each argument is a string in key:val form, which can be accessed in the generator function as shown below: + +.. code-block:: python + :name: itertools_pgen_pargs.py + :caption: itertools_pgen_pargs.py + :linenos: + + from maestrowf.datastructures.core import ParameterGenerator + import itertools as iter + + def get_custom_generator(env, **kwargs): + p_gen = ParameterGenerator() + + # Unpack any pargs passed in + size_min = int(kwargs.get('SIZE_MIN', '10')) + size_step = int(kwargs.get('SIZE_STEP', '10')) + num_sizes = int(kwargs.get('NUM_SIZES', '3')) + + sizes = range(size_min, size_min+num_sizes*size_step, size_step) + iterations = (10, 20, 30) + + size_values = [] + iteration_values = [] + trial_values = [] + + for trial, param_combo in enumerate(iter.product(sizes, iterations)): + size_values.append(param_combo[0]) + iteration_values.append(param_combo[1]) + trial_values.append(trial) + + params = { + "TRIAL": { + "values": trial_values, + "label": "TRIAL.%%" + }, + "SIZE": { + "values": size_values, + "label": "SIZE.%%" + }, + "ITER": { + "values": iteration_values, + "label": "ITER.%%" + }, + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen + +Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields the expanded parameter set: + +.. table:: Sample parameters from itertools_pgen_pargs.py + + =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== + Parameter Values + ----------- ----------------------------------------------------------- + TRIAL 0 1 2 3 4 5 6 7 8 9 10 11 + ----------- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + SIZE 10 10 10 20 20 20 30 30 30 40 40 40 + ----------- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- + ITER 10 20 30 10 20 30 10 20 30 10 20 30 + =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== From 9a65b060fbe1928845548944a7599416f8c7dba4 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 22 May 2020 19:11:40 -0700 Subject: [PATCH 03/15] Add pgen using numpy plus helper function for 1D distribution --- docs/source/parameters.rst | 65 +++++++++++++++++++++++---- docs/source/pgen_images/cheb_map.png | Bin 0 -> 35039 bytes 2 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 docs/source/pgen_images/cheb_map.png diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index 188dac5d4..3867f6a2a 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -37,12 +37,6 @@ The object simply builds the same nested key:value pairs seen in the global.para For this simple example above, this may not offer compelling advantages over writing out the flattened list in the yaml specification directly. This programmatic approach becomes preferable when expanding studies to use hundreds of parameters and parameter values or requiring non-trivial parameter value distributions. The following examples will demonstrate these scenarios using both standard python library tools and additional 3rd party packages from the larger python ecosystem. EXAMPLES: - Simple for loops and Itertools for pure python work (side by side with lulesh example) - products, permutations and combinations - pargs for dynamic generators - - section on 3rd party tools: note on virtualenvironments to make it work - numpy/scipy for chebyshev distribution: use extra function in the pgen file find another sampling algorithm: latin hypercube, or something else from scikit-learn? what about stats models? What about adding reference of env block in pgen? (not modifying, just referencing) @@ -107,12 +101,15 @@ This results in the following set of parameters, matching the lulesh sample work ITER 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== +Pgen Arguments +************** + There is an additional pgen feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the itertools_pgen.py file to change that. Maestro supports passing arguments to these generator functions on the command line: .. code-block:: bash - $ maestro run study.yaml --pgen pgen.py --parg "SIZE_MIN:10" --parg "SIZE_STEP: 10" --parg "NUM_SIZES:4" + $ maestro run study.yaml --pgen itertools_pgen_pargs.py --parg "SIZE_MIN:10" --parg "SIZE_STEP: 10" --parg "NUM_SIZES:4" Each argument is a string in key:val form, which can be accessed in the generator function as shown below: @@ -162,7 +159,7 @@ Each argument is a string in key:val form, which can be accessed in the generato for key, value in params.items(): p_gen.add_parameter(key, value["values"], value["label"]) - return p_gen + return p_gen Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields the expanded parameter set: @@ -177,3 +174,55 @@ Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields t ----------- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ITER 10 20 30 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== + +The next few examples demonstrate using 3rd party librarys and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The first is a simple parameter distribution for single variables that's encounterd in polynomial interpolation and designed to suppress the Runge and Gibbs phenomena: chebyshev points. + +.. code-block:: python + :name: np_cheb_pgen_pargs.py + :caption: np_cheb_pgen_pargs.py + :linenos: + + from maestrowf.datastructures.core import ParameterGenerator + import numpy as np + + def chebyshev_dist(var_range, num_pts): + r = 0.5*(var_range[1] - var_range[0]) + + angles = np.linspace(np.pi, 0.0, num_pts) + xpts = r*np.cos(angles) + r + ypts = r*np.sin(angles) + + return xpts + + def get_custom_generator(env, **kwargs): + p_gen = ParameterGenerator() + + # Unpack any pargs passed in + x_min = int(kwargs.get('X_MIN', '0')) + x_max = int(kwargs.get('X_MAX', '1')) + num_pts = int(kwargs.get('NUM_PTS', '10')) + + x_pts = chebyshev_dist([x_min, x_max], num_pts) + + params = { + "X": { + "values": list(x_pts), + "label": "X.%%" + }, + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen + + +Running this parameter generator with the following pargs + +.. code-block:: bash + + $ maestro run study.yaml --pgen np_cheb_pgen.py --parg "X_MIN:0" --parg "X_MAX:3" --parg "NUM_PTS:11" + +Results in the following distribution of points for the ``X`` parameter: + +.. image:: pgen_images/cheb_map.png diff --git a/docs/source/pgen_images/cheb_map.png b/docs/source/pgen_images/cheb_map.png new file mode 100644 index 0000000000000000000000000000000000000000..35ad09f29c35ed530133ce86bb2ac901c5d43517 GIT binary patch literal 35039 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 literal 0 HcmV?d00001 From 69c7bc62ac294958b0a29a20ada83ace030929d0 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 22 May 2020 19:38:16 -0700 Subject: [PATCH 04/15] Fix typos, update image --- docs/source/parameters.rst | 6 +++--- docs/source/pgen_images/cheb_map.png | Bin 35039 -> 44754 bytes 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index 3867f6a2a..d7ef036e8 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -42,7 +42,7 @@ EXAMPLES: What about adding reference of env block in pgen? (not modifying, just referencing) -First, lets use the excellent built in package itertools to generate the parameters in the lulesh example specification: +First, lets use the excellent built-in package itertools to progammatically generate the parameters in the lulesh example specification: .. code-block:: python :name: itertools_pgen.py @@ -109,7 +109,7 @@ There is an additional pgen feature that can be used to make them more dynamic. .. code-block:: bash - $ maestro run study.yaml --pgen itertools_pgen_pargs.py --parg "SIZE_MIN:10" --parg "SIZE_STEP: 10" --parg "NUM_SIZES:4" + $ maestro run study.yaml --pgen itertools_pgen_pargs.py --parg "SIZE_MIN:10" --parg "SIZE_STEP:10" --parg "NUM_SIZES:4" Each argument is a string in key:val form, which can be accessed in the generator function as shown below: @@ -223,6 +223,6 @@ Running this parameter generator with the following pargs $ maestro run study.yaml --pgen np_cheb_pgen.py --parg "X_MIN:0" --parg "X_MAX:3" --parg "NUM_PTS:11" -Results in the following distribution of points for the ``X`` parameter: +results in the 1D distribution of points for the ``X`` parameter shown by the orange circles: .. image:: pgen_images/cheb_map.png diff --git a/docs/source/pgen_images/cheb_map.png b/docs/source/pgen_images/cheb_map.png index 35ad09f29c35ed530133ce86bb2ac901c5d43517..41307e098a4f036653cb39d269fa8fb8953f393d 100644 GIT binary patch literal 44754 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 From 88975e15e29008a4fa338907e7c42f77038b708e Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 22 May 2020 23:17:55 -0700 Subject: [PATCH 05/15] Initial port of complete Parameters documentation --- docs/source/parameters.rst | 192 ++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 1 deletion(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index d7ef036e8..caedac9f4 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -1,6 +1,196 @@ Parameters ========== +Maestro supports parameterization as a means of iterating over steps in a study with varying information. Maestro uses token replacement to define variables in the study specification to be replaced when executing the study. Token replacement can be used in various context in Maestro ; however Maestro implements specific features for managing parameters. + +.. note + add (as described in Maestro Token Replacement) reference before the ; when section is added + +Maestro makes no assumptions about how parameters are defined or used in a study; this makes Maestro very flexible in regard to how a study may be configured with parameters. + +There are two ways Maestro supports parameters: + + * Directly in the study specification as the global.parameters block + + * Through the use of a user created Python module called Parameter Generator (pgen) + +.. note + add reference to this section from the hello_world examples (ref here for more thorough discussion of parameters) + similar for the token replacement sections: hello_world should be the quick start, not the exhaustive discussion + +Maestro Parameter Block +*********************** + +The quickest and easiest way to setup parameters in a Maestro study is by defining a the global.parameters block directly in the specification + +.. code-block:: yaml + :linenos: + + global.parameters: + TRIAL: + values : [1, 2, 3, 4, 5, 6, 7, 8, 9] + label : TRIAL.%% + SIZE: + values : [10, 10, 10, 20, 20, 20, 30, 30, 30] + label : SIZE.%% + ITERATIONS: + values : [10, 20, 30, 10, 20, 30, 10, 20, 30] + label : ITER.%% + +The above example defines the parameters TRIAL, SIZE, and ITERATIONS. Parameters can be used in study steps to vary information. When a parameter is defined in a study, Maestro will automatically detect the usage of a parameter moniker and handle the substitution automatically in the study expansion. This ensures that each set of parameters are run as part of the study. + +The `label` key in the block specifies the pattern to use for the directory name when the workspace is created. By default, Maestro constructs a unique workspace for each parameter combination. + +Defining multiple parameters in the parameter block will share a 1:1 mapping. Maestro requires all combinations be resolved when using the parameter block. The combinations in the above example will be expanded as follows: + + * TRIAL.1.SIZE.10.ITERATIONS.10 + + * TRIAL.2.SIZE.10.ITERATIONS.20 + + * TRIAL.3.SIZE.10.ITERATIONS.30 + + * ... + +Maestro does not do any additional operations on parameters such as cross products. If more complex methodologies are required to define parameters then the use of Maestro's Parameter Generator (pgen) is recommended. + +Defined parameters can be used in steps directly: + +.. code-block:: yaml + :linenos: + + - name: run-lulesh + description: Run LULESH. + run: + cmd: | + $(LULESH)/lulesh2.0 -s $(SIZE) -i $(ITERATIONS) -p > $(outfile) + depends: [make-lulesh] + +Even though this is defined in Maestro as a single step, Maestro will automatically run this step with each parameter combinations. This makes it very easy to setup studies and apply parameters to be run. + +.. note:: + + Maestro will only use parameters if they've been defined in at least one step + +.. note + add example with using the label in steps too + +What can be Parameterized in Maestro? +************************************* + +A common use case for Maestro is to use the parameter block to specify samples to iterate over for a simulation parameter study; however, Maestro does not make any assumptions about this use case. This makes the use of Maestro's parameter block very flexible. For example, Maestro does not require the parameter variations to be numeric. + +.. code-block:: yaml + :linenos: + :caption: study.yaml + + study: + - name: run-simulation + description: Run a simulation. + run: + cmd: | + $(CODE) -in $(SPECROOT)/$(INPUT) + depends: [] + + global.parameters: + INPUT: + values : [input1.in, input2.in, input3.in] + label : INPUT.%% + +The above example highlights a partial study spec that defines a parameter block of simulation inputs that will be varied when the study runs. The ``run-simulation`` will run three times, once for each defined input file. + +.. code-block:: yaml + :linenos: + :caption: study.yaml + + study: + - name: run-simulation + description: Run a simulation. + run: + cmd: | + $(CODE_PATH)/$(VERSION)/code.exe -in $(SPECROOT)/$(INPUT) + depends: [] + + global.parameters: + INPUT: + values : [input1.in, input2.in, input3.in, input1.in, input2.in, input3.in] + label : INPUT.%% + VERSION: + values : [4.0.0, 4.0.0, 4.0.0, 5.0.0, 5.0.0, 5.0.0] + label : VERSION.%% + +This example parameterizes the inputs and the version of the code being run. Maestro will run each input with the different code version. The above example assumes that all the code versions share a base path, ``$(CODE_PATH)`` which is inserted via the token replacment mechanism to yeild the full paths (e.g. /usr/gapps/code/4.0.0/code.exe). + +Where can Parameters be used in Study Steps? +******************************************** + +Maestro uses monikers to reference parameters in study steps, and will automatically perform token replacement on used parameters when the study is run. The page Maestro Token Replacement goes into detail about how token replacement works in Maestro. + +.. note + add maestro token replacement section reference + +Maestro is very flexible in the way it manages token replacement for parameters and as such tokens can be used in a variety of ways in a study. + +Cmd block +********* + +Parameters can be defined in the Maestro `cmd` block in the study step. Everything in Maestro's `cmd` block will be written to a bash shell or batch script (if batch is configured). Any shell commands should be valid in the `cmd` block. A common way to use parameters is to pass them in via arguments to a code, script, or tool. + +.. code-block:: yaml + :linenos: + :caption: study.yaml + + ... + + - name: run-simulation + description: Run a simulation. + run: + cmd: | + /usr/gapps/code/bin/code -in input.in -def param $(PARAM) + depends: [] + + ... + +The specific syntax for using a parameter with a specific code, script, or tool will depend on how the application supports command line arguments. + +Batch Configuration Keys +************************ + +Step based batch configurations can also be parameterized in Maestro. This provides an easy way to configure scaling studies or to manage studies where batch settings are dependent on the parameter values. + +.. code-block:: yaml + :linenos: + :caption: study.yaml + + study: + - name: run-simulation + description: Run a simulation. + run: + cmd: | + $(CODE_PATH)/$(VERSION)/code.exe -in input.in -def RES $(RES) + procs: $(PROC) + nodes: $(NODE) + walltime: $(WALLTIME) + depends: [] + + global.parameters: + RES: + values : [2, 4, 6, 8] + label : RES.%% + PROC: + values : [8, 8, 16, 32] + label : PROC.%% + NODE: + values : [1, 1, 2, 4] + label : NODE.%% + WALLTIME: + values : ["00:10:00", "00:15:00", "00:30:00", "01:00:00"] + label : PROC.%% + +.. note + Make the sub sections appear as subsections for the relevent blocks (cmd block, batch configurations..) + + Add some dag graphs in here at some point? + Parameter Generator (pgen) ************************** @@ -175,7 +365,7 @@ Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields t ITER 10 20 30 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== -The next few examples demonstrate using 3rd party librarys and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The first is a simple parameter distribution for single variables that's encounterd in polynomial interpolation and designed to suppress the Runge and Gibbs phenomena: chebyshev points. +The next example demonstrates using 3rd party librarys and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. There are no requirements to cram all of the logic into the ``get_custom_generator`` function, so we'll build the sampling logic into a helper function instead. The simple parameter distribution demoed in this example is for single variables and is encounterd in polynomial interpolation applications and is designed to suppress the Runge and Gibbs phenomena by sampling the funciton at the chebyshev points. .. code-block:: python :name: np_cheb_pgen_pargs.py From c9a1672e8c4faccf2b0ed2291059cb08a6741749 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Fri, 22 May 2020 23:49:54 -0700 Subject: [PATCH 06/15] Fix up api docs warnings, add missing script adapters --- docs/source/maestrowf.abstracts.rst | 8 ---- docs/source/maestrowf.interfaces.script.rst | 16 +++++++ maestrowf/abstracts/envobject.py | 14 +++--- maestrowf/abstracts/specification.py | 2 +- maestrowf/conductor.py | 2 +- maestrowf/datastructures/yamlspecification.py | 2 +- maestrowf/interfaces/script/__init__.py | 8 ++-- .../interfaces/script/fluxscriptadapter.py | 44 +++++++++---------- .../interfaces/script/lsfscriptadapter.py | 24 +++++----- 9 files changed, 65 insertions(+), 55 deletions(-) diff --git a/docs/source/maestrowf.abstracts.rst b/docs/source/maestrowf.abstracts.rst index c4bd008ca..11aa5324d 100644 --- a/docs/source/maestrowf.abstracts.rst +++ b/docs/source/maestrowf.abstracts.rst @@ -41,14 +41,6 @@ maestrowf.abstracts.graph module :undoc-members: :show-inheritance: -maestrowf.abstracts.simobject module ------------------------------------- - -.. automodule:: maestrowf.abstracts.simobject - :members: - :undoc-members: - :show-inheritance: - maestrowf.abstracts.specification module ---------------------------------------- diff --git a/docs/source/maestrowf.interfaces.script.rst b/docs/source/maestrowf.interfaces.script.rst index e4b2447a8..293350a04 100644 --- a/docs/source/maestrowf.interfaces.script.rst +++ b/docs/source/maestrowf.interfaces.script.rst @@ -25,4 +25,20 @@ maestrowf.interfaces.script.slurmscriptadapter module :undoc-members: :show-inheritance: +maestrowf.interfaces.script.lsfscriptadapter module +----------------------------------------------------- + +.. automodule:: maestrowf.interfaces.script.lsfscriptadapter + :members: + :undoc-members: + :show-inheritance: + +maestrowf.interfaces.script.fluxscriptadapter module +----------------------------------------------------- + +.. automodule:: maestrowf.interfaces.script.fluxscriptadapter + :members: + :undoc-members: + :show-inheritance: + diff --git a/maestrowf/abstracts/envobject.py b/maestrowf/abstracts/envobject.py index 32e1685c6..aa629d18f 100644 --- a/maestrowf/abstracts/envobject.py +++ b/maestrowf/abstracts/envobject.py @@ -101,8 +101,9 @@ class Source(EnvObject): The Source environment class is meant to provide a way to programmatically set environment settings that binaries or other scripts may require in the workflow. Such settings that are intended to be captured are: - - Exporting of shell/environment variables (using 'export') - - Setting of an environment package with the 'use' command + + * Exporting of shell/environment variables (using 'export') + * Setting of an environment package with the 'use' command """ @abstractmethod @@ -130,11 +131,12 @@ class Dependency(Substitution): The Dependency base class is intended to be used to capture external items the workflow is dependent on. These items include (but are not limited to): - - Remotely stored repositories (such as bitbucket) - - Paths located on the filesystem that hold required files or binaries - - Binaries that are required to be installed using a specific package + + * Remotely stored repositories (such as bitbucket) + * Paths located on the filesystem that hold required files or binaries + * Binaries that are required to be installed using a specific package manager - - External APIs that a workflow needs to pull data from + * External APIs that a workflow needs to pull data from The goal of this base class is to make it so that this package is able to pull exernal dependencies in a consistent manner. diff --git a/maestrowf/abstracts/specification.py b/maestrowf/abstracts/specification.py index c6c9dbb98..d613f464f 100644 --- a/maestrowf/abstracts/specification.py +++ b/maestrowf/abstracts/specification.py @@ -46,7 +46,7 @@ def load_specification(cls, path): :param path: Path to a study specification. :returns: A specification object containing the information loaded - from path. + from path. """ @abstractclassmethod diff --git a/maestrowf/conductor.py b/maestrowf/conductor.py index 901d00270..c322e8486 100644 --- a/maestrowf/conductor.py +++ b/maestrowf/conductor.py @@ -295,7 +295,7 @@ def initialize(self, batch_info, sleeptime=60): :param batch_info: A dict containing batch information. :param sleeptime: The amount of sleep time between polling loops - [Default: 60s]. + [Default: 60s]. """ # Set our conductor's sleep time. self.sleep_time = sleeptime diff --git a/maestrowf/datastructures/yamlspecification.py b/maestrowf/datastructures/yamlspecification.py index 57fe87aa5..8e4529446 100644 --- a/maestrowf/datastructures/yamlspecification.py +++ b/maestrowf/datastructures/yamlspecification.py @@ -126,7 +126,7 @@ def load_specification_from_stream(cls, stream): :param stream: Raw text stream to study YAML specification data. :returns: A specification object containing the information from the - passed stream. + passed stream. """ try: diff --git a/maestrowf/interfaces/script/__init__.py b/maestrowf/interfaces/script/__init__.py index bb253b963..72c50d197 100644 --- a/maestrowf/interfaces/script/__init__.py +++ b/maestrowf/interfaces/script/__init__.py @@ -59,7 +59,7 @@ def job_identifier(self): Property for the job identifier for the record. :returns: A string representing the job identifer assigned by the - scheduler. + scheduler. """ return self._info.get("jobid", None) @@ -69,7 +69,7 @@ def submission_code(self): Property for submission state for the record. :returns: A SubmissionCode enum representing the state of the - submission call. + submission call. """ return self._subcode @@ -79,7 +79,7 @@ def return_code(self): Property for the raw return code returned from submission. :returns: An integer representing the state of the raw return code - from submission. + from submission. """ return self._info["retcode"] @@ -111,7 +111,7 @@ def add_status(self, jobid, cancel_status): :param jobid: Unique job identifier for the job status to be added. :param cancel_status: CancelCode designating how cancellation - terminated. + terminated. """ if not isinstance(cancel_status, CancelCode): raise TypeError( diff --git a/maestrowf/interfaces/script/fluxscriptadapter.py b/maestrowf/interfaces/script/fluxscriptadapter.py index 7da760ffd..b7f7e0816 100644 --- a/maestrowf/interfaces/script/fluxscriptadapter.py +++ b/maestrowf/interfaces/script/fluxscriptadapter.py @@ -74,10 +74,10 @@ def __init__(self, **kwargs): The expected keyword arguments that are expected when the Flux adapter is instantiated are as follows: - - host: The cluster to execute scripts on. - - bank: The account to charge computing time to. - - queue: Scheduler queue scripts should be submitted to. - - nodes: The number of compute nodes to be reserved for computing. + * host: The cluster to execute scripts on. + * bank: The account to charge computing time to. + * queue: Scheduler queue scripts should be submitted to. + * nodes: The number of compute nodes to be reserved for computing. :param **kwargs: A dictionary with default settings for the adapter. """ @@ -119,7 +119,7 @@ def get_header(self, step): :param step: A StudyStep instance. :returns: A string of the header based on internal batch parameters and - the parameter step. + the parameter step. """ run = dict(step.run) batch_header = dict(self._batch) @@ -155,9 +155,9 @@ def get_parallelize_command(self, procs, nodes=None, **kwargs): :param procs: Number of processors to allocate to the parallel call. :param nodes: Number of nodes to allocate to the parallel call - (default = 1). + (default = 1). :returns: A string of the parallelize command configured using nodes - and procs. + and procs. """ args = [ "env", @@ -185,10 +185,10 @@ def submit(self, step, path, cwd, job_map=None, env=None): :param path: Local path to the script to be executed. :param cwd: Path to the current working directory. :param job_map: A dictionary mapping step names to their job - identifiers. + identifiers. :param env: A dict containing a modified environment for execution. :returns: The return status of the submission command and job - identiifer. + identiifer. """ # # Leading command is "sbatch" # cmd = ["flux", "submit"] @@ -277,7 +277,7 @@ def check_jobs(self, joblist): :param joblist: A list of job identifiers to be queried. :returns: The return code of the status query, and a dictionary of job - identifiers to their status. + identifiers to their status. """ LOGGER.debug("Joblist type -- %s", type(joblist)) LOGGER.debug("Joblist contents -- %s", joblist) @@ -494,10 +494,10 @@ def __init__(self, **kwargs): The expected keyword arguments that are expected when the Flux adapter is instantiated are as follows: - - host: The cluster to execute scripts on. - - bank: The account to charge computing time to. - - queue: Scheduler queue scripts should be submitted to. - - nodes: The number of compute nodes to be reserved for computing. + * host: The cluster to execute scripts on. + * bank: The account to charge computing time to. + * queue: Scheduler queue scripts should be submitted to. + * nodes: The number of compute nodes to be reserved for computing. :param **kwargs: A dictionary with default settings for the adapter. """ @@ -537,7 +537,7 @@ def get_header(self, step): :param step: A StudyStep instance. :returns: A string of the header based on internal batch parameters and - the parameter step. + the parameter step. """ run = dict(step.run) batch_header = dict(self._batch) @@ -565,9 +565,9 @@ def get_parallelize_command(self, procs, nodes=None, **kwargs): :param procs: Number of processors to allocate to the parallel call. :param nodes: Number of nodes to allocate to the parallel call - (default = 1). + (default = 1). :returns: A string of the parallelize command configured using nodes - and procs. + and procs. """ args = ["flux", "wreckrun", "-n", str(procs)] @@ -590,10 +590,10 @@ def submit(self, step, path, cwd, job_map=None, env=None): :param path: Local path to the script to be executed. :param cwd: Path to the current working directory. :param job_map: A dictionary mapping step names to their job - identifiers. + identifiers. :param env: A dict containing a modified environment for execution. :returns: The return status of the submission command and job - identiifer. + identiifer. """ walltime = self._convert_walltime_to_seconds(step.run["walltime"]) nodes = step.run.get("nodes") @@ -680,7 +680,7 @@ def check_jobs(self, joblist): :param joblist: A list of job identifiers to be queried. :returns: The return code of the status query, and a dictionary of job - identifiers to their status. + identifiers to their status. """ LOGGER.debug("Joblist type -- %s", type(joblist)) LOGGER.debug("Joblist contents -- %s", joblist) @@ -848,8 +848,8 @@ def _write_script(self, ws_path, step): :param ws_path: Path to the workspace directory of the step. :param step: An instance of a StudyStep. :returns: Boolean value (True if to be scheduled), the path to the - written script for run["cmd"], and the path to the script written for - run["restart"] (if it exists). + written script for run["cmd"], and the path to the script + written for run["restart"] (if it exists). """ to_be_scheduled, cmd, restart = self.get_scheduler_command(step) diff --git a/maestrowf/interfaces/script/lsfscriptadapter.py b/maestrowf/interfaces/script/lsfscriptadapter.py index d18af67c4..c444fff1c 100644 --- a/maestrowf/interfaces/script/lsfscriptadapter.py +++ b/maestrowf/interfaces/script/lsfscriptadapter.py @@ -60,10 +60,10 @@ def __init__(self, **kwargs): The expected keyword arguments that are expected when the Slurm adapter is instantiated are as follows: - - host: The cluster to execute scripts on. - - bank: The account to charge computing time to. - - queue: Scheduler queue scripts should be submitted to. - - tasks: The number of compute nodes to be reserved for computing. + * host: The cluster to execute scripts on. + * bank: The account to charge computing time to. + * queue: Scheduler queue scripts should be submitted to. + * tasks: The number of compute nodes to be reserved for computing. :param **kwargs: A dictionary with default settings for the adapter. """ @@ -101,7 +101,7 @@ def get_header(self, step): :param step: A StudyStep instance. :returns: A string of the header based on internal batch parameters and - the parameter step. + the parameter step. """ run = dict(step.run) batch_header = dict(self._batch) @@ -138,9 +138,9 @@ def get_parallelize_command(self, procs, nodes=None, **kwargs): :param procs: Number of processors to allocate to the parallel call. :param nodes: Number of nodes to allocate to the parallel call - (default = 1). + (default = 1). :returns: A string of the parallelize command configured using nodes - and procs. + and procs. """ args = [self._cmd_flags["cmd"]] @@ -177,10 +177,10 @@ def submit(self, step, path, cwd, job_map=None, env=None): :param path: Local path to the script to be executed. :param cwd: Path to the current working directory. :param job_map: A dictionary mapping step names to their job - identifiers. + identifiers. :param env: A dict containing a modified environment for execution. :returns: The return status of the submission command and job - identiifer. + identiifer. """ args = ["bsub"] @@ -221,7 +221,7 @@ def check_jobs(self, joblist): :param joblist: A list of job identifiers to be queried. :returns: The return code of the status query, and a dictionary of job - identifiers to their status. + identifiers to their status. """ # TODO: This method needs to be updated to use sacct. # squeue options: @@ -376,8 +376,8 @@ def _write_script(self, ws_path, step): :param ws_path: Path to the workspace directory of the step. :param step: An instance of a StudyStep. :returns: Boolean value (True if to be scheduled), the path to the - written script for run["cmd"], and the path to the script written for - run["restart"] (if it exists). + written script for run["cmd"], and the path to the script + written for run["restart"] (if it exists). """ to_be_scheduled, cmd, restart = self.get_scheduler_command(step) From 74ab917c489141afdc96d3c9ea1943310876ee13 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Sat, 23 May 2020 15:08:46 -0700 Subject: [PATCH 07/15] Add subsection on accessing env block variables inside pgen --- docs/source/parameters.rst | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index caedac9f4..d0a196d54 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -365,6 +365,8 @@ Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields t ITER 10 20 30 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== +Notice that using the pgen input method makes it trivially easy to add 1000's of parameters, something which would be cumbersome via manual editing of the global.parameters block in the study specification file. + The next example demonstrates using 3rd party librarys and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. There are no requirements to cram all of the logic into the ``get_custom_generator`` function, so we'll build the sampling logic into a helper function instead. The simple parameter distribution demoed in this example is for single variables and is encounterd in polynomial interpolation applications and is designed to suppress the Runge and Gibbs phenomena by sampling the funciton at the chebyshev points. .. code-block:: python @@ -416,3 +418,60 @@ Running this parameter generator with the following pargs results in the 1D distribution of points for the ``X`` parameter shown by the orange circles: .. image:: pgen_images/cheb_map.png + + +Env Block +********* + +In addition to command line arguments via ``pargs``, the variables defined in the env block in the workflow specification file can be accessed inside the ParameterGenerator objects, which is passed in to ``get_custom_generator`` as the first argument. The lulesh sample specification can be extended to store the default values for the pgen, enhancing the reproducability of the generator. The following example extends the lulesh_monte_carlo_args.py sample generator and adds one additional env var, `seed`, which can be overriden via ``pargs``, making the default configuration a fully repeatable study specification. The variables are accessed via the :py:class:`~maestrowf.datastructures.core.StudyEnvironment`'s :py:func:`~maestrowf.datastructures.core.StudyEnvironment.find()` function, which will return ``None`` if the variable is not defined in the study specification. + +.. note + possible to link the sample files directly? -> also, might be good to have them pulled into the docs automatically + somewhere.. + +.. code-block:: python + :linenos: + :caption: lulesh_monte_carlo_args_mod.py + + from random import randint + + from maestrowf.datastructures.core import ParameterGenerator + + + def get_custom_generator(env, **kwargs): + """ + Create a custom populated ParameterGenerator. + This function recreates the exact same parameter set as the sample LULESH + specifications. The point of this file is to present an example of how to + generate custom parameters. + :params kwargs: A dictionary of keyword arguments this function uses. + :returns: A ParameterGenerator populated with parameters. + """ + p_gen = ParameterGenerator() + trials = int(kwargs.get("trials", env.find("TRIALS").value)) + size_min = int(kwargs.get("smin", env.find("SMIN").value)) + size_max = int(kwargs.get("smax", env.find("SMAX").value)) + iterations = int(kwargs.get("iter", env.find("ITER").value)) + seed = kwargs.get("seed", env.find("SEED").value) + + random.seed(a=seed) + + params = { + "TRIAL": { + "values": [i for i in range(1, trials)], + "label": "TRIAL.%%" + }, + "SIZE": { + "values": [randint(size_min, size_max) for i in range(1, trials)], + "label": "SIZE.%%" + }, + "ITERATIONS": { + "values": [iterations for i in range(1, trials)], + "label": "ITERATIONS.%%" + } + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen From 8fd16dea6782e0539caeb19fb181180426107dd2 Mon Sep 17 00:00:00 2001 From: Frank Di Natale Date: Sun, 24 May 2020 22:52:12 -0700 Subject: [PATCH 08/15] Literal imports and some style tweaks. --- docs/source/parameters.rst | 156 +++++------------- samples/parameterization/itertools_pgen.py | 38 +++++ .../lulesh_montecarlo_args.py | 8 +- .../parameterization/np_cheb_pgen_pargs.py | 35 ++++ 4 files changed, 119 insertions(+), 118 deletions(-) create mode 100644 samples/parameterization/itertools_pgen.py create mode 100644 samples/parameterization/np_cheb_pgen_pargs.py diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index d0a196d54..8753cdc38 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -1,5 +1,5 @@ -Parameters -========== +Specifying Study Parameters +=========================== Maestro supports parameterization as a means of iterating over steps in a study with varying information. Maestro uses token replacement to define variables in the study specification to be replaced when executing the study. Token replacement can be used in various context in Maestro ; however Maestro implements specific features for managing parameters. @@ -11,7 +11,7 @@ Maestro makes no assumptions about how parameters are defined or used in a study There are two ways Maestro supports parameters: * Directly in the study specification as the global.parameters block - + * Through the use of a user created Python module called Parameter Generator (pgen) .. note @@ -37,7 +37,7 @@ The quickest and easiest way to setup parameters in a Maestro study is by defini values : [10, 20, 30, 10, 20, 30, 10, 20, 30] label : ITER.%% -The above example defines the parameters TRIAL, SIZE, and ITERATIONS. Parameters can be used in study steps to vary information. When a parameter is defined in a study, Maestro will automatically detect the usage of a parameter moniker and handle the substitution automatically in the study expansion. This ensures that each set of parameters are run as part of the study. +The above example defines the parameters TRIAL, SIZE, and ITERATIONS. Parameters can be used in study steps to vary information. When a parameter is defined in a study, Maestro will automatically detect the usage of a parameter moniker and handle the substitution automatically in the study expansion. This ensures that each set of parameters are run as part of the study. The `label` key in the block specifies the pattern to use for the directory name when the workspace is created. By default, Maestro constructs a unique workspace for each parameter combination. @@ -90,7 +90,7 @@ A common use case for Maestro is to use the parameter block to specify samples t cmd: | $(CODE) -in $(SPECROOT)/$(INPUT) depends: [] - + global.parameters: INPUT: values : [input1.in, input2.in, input3.in] @@ -101,7 +101,7 @@ The above example highlights a partial study spec that defines a parameter block .. code-block:: yaml :linenos: :caption: study.yaml - + study: - name: run-simulation description: Run a simulation. @@ -109,7 +109,7 @@ The above example highlights a partial study spec that defines a parameter block cmd: | $(CODE_PATH)/$(VERSION)/code.exe -in $(SPECROOT)/$(INPUT) depends: [] - + global.parameters: INPUT: values : [input1.in, input2.in, input3.in, input1.in, input2.in, input3.in] @@ -140,7 +140,7 @@ Parameters can be defined in the Maestro `cmd` block in the study step. Everythi :caption: study.yaml ... - + - name: run-simulation description: Run a simulation. run: @@ -149,7 +149,7 @@ Parameters can be defined in the Maestro `cmd` block in the study step. Everythi depends: [] ... - + The specific syntax for using a parameter with a specific code, script, or tool will depend on how the application supports command line arguments. Batch Configuration Keys @@ -171,7 +171,7 @@ Step based batch configurations can also be parameterized in Maestro. This provi nodes: $(NODE) walltime: $(WALLTIME) depends: [] - + global.parameters: RES: values : [2, 4, 6, 8] @@ -182,7 +182,7 @@ Step based batch configurations can also be parameterized in Maestro. This provi NODE: values : [1, 1, 2, 4] label : NODE.%% - WALLTIME: + WALLTIME: values : ["00:10:00", "00:15:00", "00:30:00", "01:00:00"] label : PROC.%% @@ -206,7 +206,7 @@ The minimum requirements for making a valid pgen file is to make a function call :linenos: from maestrowf.datastructures.core import ParameterGenerator - + def get_custom_generator(env, **kwargs): p_gen = ParameterGenerator() params = { @@ -215,10 +215,10 @@ The minimum requirements for making a valid pgen file is to make a function call "label": "COUNT.%%" }, } - + for key, value in params.items(): p_gen.add_parameter(key, value["values"], value["label"]) - + return p_gen @@ -229,10 +229,9 @@ For this simple example above, this may not offer compelling advantages over wri EXAMPLES: find another sampling algorithm: latin hypercube, or something else from scikit-learn? what about stats models? -What about adding reference of env block in pgen? (not modifying, just referencing) - -First, lets use the excellent built-in package itertools to progammatically generate the parameters in the lulesh example specification: +EXAMPLE: + Using Python's standard ``itertools`` package to perform a Cartesian Product of parameters in the lulesh example specification. .. code-block:: python :name: itertools_pgen.py @@ -241,7 +240,7 @@ First, lets use the excellent built-in package itertools to progammatically gene from maestrowf.datastructures.core import ParameterGenerator import itertools as iter - + def get_custom_generator(env, **kwargs): p_gen = ParameterGenerator() @@ -251,17 +250,17 @@ First, lets use the excellent built-in package itertools to progammatically gene size_values = [] iteration_values = [] trial_values = [] - + for trial, param_combo in enumerate(iter.product(sizes, iterations)): size_values.append(param_combo[0]) iteration_values.append(param_combo[1]) trial_values.append(trial) - + params = { "TRIAL": { "values": trial_values, "label": "TRIAL.%%" - }, + }, "SIZE": { "values": size_values, "label": "SIZE.%%" @@ -269,13 +268,13 @@ First, lets use the excellent built-in package itertools to progammatically gene "ITER": { "values": iteration_values, "label": "ITER.%%" - }, + }, } for key, value in params.items(): p_gen.add_parameter(key, value["values"], value["label"]) - - return p_gen + + return p_gen This results in the following set of parameters, matching the lulesh sample workflow: @@ -301,7 +300,7 @@ There is an additional pgen feature that can be used to make them more dynamic. $ maestro run study.yaml --pgen itertools_pgen_pargs.py --parg "SIZE_MIN:10" --parg "SIZE_STEP:10" --parg "NUM_SIZES:4" -Each argument is a string in key:val form, which can be accessed in the generator function as shown below: +Each argument is a string in ``key:value`` form, which can be accessed in the generator function as shown below: .. code-block:: python :name: itertools_pgen_pargs.py @@ -310,7 +309,7 @@ Each argument is a string in key:val form, which can be accessed in the generato from maestrowf.datastructures.core import ParameterGenerator import itertools as iter - + def get_custom_generator(env, **kwargs): p_gen = ParameterGenerator() @@ -318,24 +317,24 @@ Each argument is a string in key:val form, which can be accessed in the generato size_min = int(kwargs.get('SIZE_MIN', '10')) size_step = int(kwargs.get('SIZE_STEP', '10')) num_sizes = int(kwargs.get('NUM_SIZES', '3')) - + sizes = range(size_min, size_min+num_sizes*size_step, size_step) iterations = (10, 20, 30) size_values = [] iteration_values = [] trial_values = [] - + for trial, param_combo in enumerate(iter.product(sizes, iterations)): size_values.append(param_combo[0]) iteration_values.append(param_combo[1]) trial_values.append(trial) - + params = { "TRIAL": { "values": trial_values, "label": "TRIAL.%%" - }, + }, "SIZE": { "values": size_values, "label": "SIZE.%%" @@ -343,12 +342,12 @@ Each argument is a string in key:val form, which can be accessed in the generato "ITER": { "values": iteration_values, "label": "ITER.%%" - }, + }, } for key, value in params.items(): p_gen.add_parameter(key, value["values"], value["label"]) - + return p_gen Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields the expanded parameter set: @@ -367,50 +366,19 @@ Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields t Notice that using the pgen input method makes it trivially easy to add 1000's of parameters, something which would be cumbersome via manual editing of the global.parameters block in the study specification file. -The next example demonstrates using 3rd party librarys and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. There are no requirements to cram all of the logic into the ``get_custom_generator`` function, so we'll build the sampling logic into a helper function instead. The simple parameter distribution demoed in this example is for single variables and is encounterd in polynomial interpolation applications and is designed to suppress the Runge and Gibbs phenomena by sampling the funciton at the chebyshev points. +The next example demonstrates using 3rd party libraries and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. There are no requirements to cram all of the logic into the ``get_custom_generator`` function, so we'll build the sampling logic into a helper function instead. The simple parameter distribution demoed in this example is for single variables and is encountered in polynomial interpolation applications and is designed to suppress the Runge and Gibbs phenomena by sampling the function at the Chebyshev nodes. -.. code-block:: python - :name: np_cheb_pgen_pargs.py +EXAMPLE: + Using ``numpy`` to calculate a sampling of a function at the Chebyshev nodes. + +.. literalinclude:: ../../samples/parameterization/np_cheb_pgen_pargs.py + :language: python :caption: np_cheb_pgen_pargs.py :linenos: - - from maestrowf.datastructures.core import ParameterGenerator - import numpy as np - def chebyshev_dist(var_range, num_pts): - r = 0.5*(var_range[1] - var_range[0]) - angles = np.linspace(np.pi, 0.0, num_pts) - xpts = r*np.cos(angles) + r - ypts = r*np.sin(angles) - - return xpts - - def get_custom_generator(env, **kwargs): - p_gen = ParameterGenerator() - - # Unpack any pargs passed in - x_min = int(kwargs.get('X_MIN', '0')) - x_max = int(kwargs.get('X_MAX', '1')) - num_pts = int(kwargs.get('NUM_PTS', '10')) - - x_pts = chebyshev_dist([x_min, x_max], num_pts) - - params = { - "X": { - "values": list(x_pts), - "label": "X.%%" - }, - } - - for key, value in params.items(): - p_gen.add_parameter(key, value["values"], value["label"]) - - return p_gen - - Running this parameter generator with the following pargs - + .. code-block:: bash $ maestro run study.yaml --pgen np_cheb_pgen.py --parg "X_MIN:0" --parg "X_MAX:3" --parg "NUM_PTS:11" @@ -429,49 +397,7 @@ In addition to command line arguments via ``pargs``, the variables defined in th possible to link the sample files directly? -> also, might be good to have them pulled into the docs automatically somewhere.. -.. code-block:: python +.. literalinclude:: ../../samples/parameterization/lulesh_montecarlo_args.py + :language: python :linenos: - :caption: lulesh_monte_carlo_args_mod.py - - from random import randint - - from maestrowf.datastructures.core import ParameterGenerator - - - def get_custom_generator(env, **kwargs): - """ - Create a custom populated ParameterGenerator. - This function recreates the exact same parameter set as the sample LULESH - specifications. The point of this file is to present an example of how to - generate custom parameters. - :params kwargs: A dictionary of keyword arguments this function uses. - :returns: A ParameterGenerator populated with parameters. - """ - p_gen = ParameterGenerator() - trials = int(kwargs.get("trials", env.find("TRIALS").value)) - size_min = int(kwargs.get("smin", env.find("SMIN").value)) - size_max = int(kwargs.get("smax", env.find("SMAX").value)) - iterations = int(kwargs.get("iter", env.find("ITER").value)) - seed = kwargs.get("seed", env.find("SEED").value) - - random.seed(a=seed) - - params = { - "TRIAL": { - "values": [i for i in range(1, trials)], - "label": "TRIAL.%%" - }, - "SIZE": { - "values": [randint(size_min, size_max) for i in range(1, trials)], - "label": "SIZE.%%" - }, - "ITERATIONS": { - "values": [iterations for i in range(1, trials)], - "label": "ITERATIONS.%%" - } - } - - for key, value in params.items(): - p_gen.add_parameter(key, value["values"], value["label"]) - - return p_gen + :caption: lulesh_montecarlo_args.py diff --git a/samples/parameterization/itertools_pgen.py b/samples/parameterization/itertools_pgen.py new file mode 100644 index 000000000..58c7495f1 --- /dev/null +++ b/samples/parameterization/itertools_pgen.py @@ -0,0 +1,38 @@ +from maestrowf.datastructures.core import ParameterGenerator +import itertools as iter + + +def get_custom_generator(env, **kwargs): + p_gen = ParameterGenerator() + + sizes = (10, 20, 30) + iterations = (10, 20, 30) + + size_values = [] + iteration_values = [] + trial_values = [] + + for trial, param_combo in enumerate(iter.product(sizes, iterations)): + size_values.append(param_combo[0]) + iteration_values.append(param_combo[1]) + trial_values.append(trial) + + params = { + "TRIAL": { + "values": trial_values, + "label": "TRIAL.%%" + }, + "SIZE": { + "values": size_values, + "label": "SIZE.%%" + }, + "ITER": { + "values": iteration_values, + "label": "ITER.%%" + }, + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen diff --git a/samples/parameterization/lulesh_montecarlo_args.py b/samples/parameterization/lulesh_montecarlo_args.py index a46871679..574495f16 100644 --- a/samples/parameterization/lulesh_montecarlo_args.py +++ b/samples/parameterization/lulesh_montecarlo_args.py @@ -1,6 +1,6 @@ """An example file that produces a custom parameters for the LULESH example.""" -from random import randint +from random import randint, seed from maestrowf.datastructures.core import ParameterGenerator @@ -8,11 +8,9 @@ def get_custom_generator(env, **kwargs): """ Create a custom populated ParameterGenerator. - This function recreates the exact same parameter set as the sample LULESH specifications. The point of this file is to present an example of how to generate custom parameters. - :params kwargs: A dictionary of keyword arguments this function uses. :returns: A ParameterGenerator populated with parameters. """ @@ -21,6 +19,10 @@ def get_custom_generator(env, **kwargs): size_min = int(kwargs.get("smin", env.find("SMIN").value)) size_max = int(kwargs.get("smax", env.find("SMAX").value)) iterations = int(kwargs.get("iter", env.find("ITER").value)) + r_seed = kwargs.get("seed", env.find("SEED").value) + + seed(a=r_seed) + params = { "TRIAL": { "values": [i for i in range(1, trials)], diff --git a/samples/parameterization/np_cheb_pgen_pargs.py b/samples/parameterization/np_cheb_pgen_pargs.py new file mode 100644 index 000000000..21f83bd09 --- /dev/null +++ b/samples/parameterization/np_cheb_pgen_pargs.py @@ -0,0 +1,35 @@ +from maestrowf.datastructures.core import ParameterGenerator +import numpy as np + + +def chebyshev_dist(var_range, num_pts): + r = 0.5*(var_range[1] - var_range[0]) + + angles = np.linspace(np.pi, 0.0, num_pts) + xpts = r*np.cos(angles) + r + ypts = r*np.sin(angles) + + return xpts, ypts + + +def get_custom_generator(env, **kwargs): + p_gen = ParameterGenerator() + + # Unpack any pargs passed in + x_min = int(kwargs.get('X_MIN', '0')) + x_max = int(kwargs.get('X_MAX', '1')) + num_pts = int(kwargs.get('NUM_PTS', '10')) + + x_pts, ypts = chebyshev_dist([x_min, x_max], num_pts) + + params = { + "X": { + "values": list(x_pts), + "label": "X.%%" + }, + } + + for key, value in params.items(): + p_gen.add_parameter(key, value["values"], value["label"]) + + return p_gen From a38a4883d1476fe049adcbddd092fcf455bdee67 Mon Sep 17 00:00:00 2001 From: Frank Di Natale Date: Mon, 25 May 2020 22:28:44 -0700 Subject: [PATCH 09/15] Some minor title and header tweaks. --- docs/source/parameters.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index 8753cdc38..254929051 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -131,7 +131,7 @@ Maestro uses monikers to reference parameters in study steps, and will automatic Maestro is very flexible in the way it manages token replacement for parameters and as such tokens can be used in a variety of ways in a study. Cmd block -********* +--------- Parameters can be defined in the Maestro `cmd` block in the study step. Everything in Maestro's `cmd` block will be written to a bash shell or batch script (if batch is configured). Any shell commands should be valid in the `cmd` block. A common way to use parameters is to pass them in via arguments to a code, script, or tool. @@ -153,7 +153,7 @@ Parameters can be defined in the Maestro `cmd` block in the study step. Everythi The specific syntax for using a parameter with a specific code, script, or tool will depend on how the application supports command line arguments. Batch Configuration Keys -************************ +------------------------ Step based batch configurations can also be parameterized in Maestro. This provides an easy way to configure scaling studies or to manage studies where batch settings are dependent on the parameter values. @@ -290,8 +290,8 @@ This results in the following set of parameters, matching the lulesh sample work ITER 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== -Pgen Arguments -************** +Pgen Arguments (pargs) +********************** There is an additional pgen feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the itertools_pgen.py file to change that. Maestro supports passing arguments to these generator functions on the command line: @@ -388,8 +388,8 @@ results in the 1D distribution of points for the ``X`` parameter shown by the or .. image:: pgen_images/cheb_map.png -Env Block -********* +Referencing Values from a Specification's Env Block +*************************************************** In addition to command line arguments via ``pargs``, the variables defined in the env block in the workflow specification file can be accessed inside the ParameterGenerator objects, which is passed in to ``get_custom_generator`` as the first argument. The lulesh sample specification can be extended to store the default values for the pgen, enhancing the reproducability of the generator. The following example extends the lulesh_monte_carlo_args.py sample generator and adds one additional env var, `seed`, which can be overriden via ``pargs``, making the default configuration a fully repeatable study specification. The variables are accessed via the :py:class:`~maestrowf.datastructures.core.StudyEnvironment`'s :py:func:`~maestrowf.datastructures.core.StudyEnvironment.find()` function, which will return ``None`` if the variable is not defined in the study specification. From 6eba6030d48abe7857bd1d10d616e85b4555e9f0 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 26 May 2020 09:54:08 -0700 Subject: [PATCH 10/15] Remove out of date notes --- docs/source/parameters.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index d0a196d54..9ab29b312 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -226,12 +226,6 @@ The object simply builds the same nested key:value pairs seen in the global.para For this simple example above, this may not offer compelling advantages over writing out the flattened list in the yaml specification directly. This programmatic approach becomes preferable when expanding studies to use hundreds of parameters and parameter values or requiring non-trivial parameter value distributions. The following examples will demonstrate these scenarios using both standard python library tools and additional 3rd party packages from the larger python ecosystem. -EXAMPLES: - find another sampling algorithm: latin hypercube, or something else from scikit-learn? what about stats models? - -What about adding reference of env block in pgen? (not modifying, just referencing) - - First, lets use the excellent built-in package itertools to progammatically generate the parameters in the lulesh example specification: .. code-block:: python From ac34eb67ae37d1ef600e1fba9cde47a14006f975 Mon Sep 17 00:00:00 2001 From: Frank Di Natale Date: Tue, 26 May 2020 11:00:10 -0700 Subject: [PATCH 11/15] Renamed itertools_pgen to reference LULESH. --- docs/source/parameters.rst | 44 ++----------------- ...tools_pgen.py => lulesh_itertools_pgen.py} | 0 2 files changed, 3 insertions(+), 41 deletions(-) rename samples/parameterization/{itertools_pgen.py => lulesh_itertools_pgen.py} (100%) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index 8de29a45a..dd8c0cf98 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -229,49 +229,11 @@ For this simple example above, this may not offer compelling advantages over wri EXAMPLE: Using Python's standard ``itertools`` package to perform a Cartesian Product of parameters in the lulesh example specification. -.. code-block:: python - :name: itertools_pgen.py - :caption: itertools_pgen.py +.. literalinclude:: ../../samples/parameterization/lulesh_itertools_pgen.py + :language: python + :caption: lulesh_itertools_pgen.py :linenos: - from maestrowf.datastructures.core import ParameterGenerator - import itertools as iter - - def get_custom_generator(env, **kwargs): - p_gen = ParameterGenerator() - - sizes = (10, 20, 30) - iterations = (10, 20, 30) - - size_values = [] - iteration_values = [] - trial_values = [] - - for trial, param_combo in enumerate(iter.product(sizes, iterations)): - size_values.append(param_combo[0]) - iteration_values.append(param_combo[1]) - trial_values.append(trial) - - params = { - "TRIAL": { - "values": trial_values, - "label": "TRIAL.%%" - }, - "SIZE": { - "values": size_values, - "label": "SIZE.%%" - }, - "ITER": { - "values": iteration_values, - "label": "ITER.%%" - }, - } - - for key, value in params.items(): - p_gen.add_parameter(key, value["values"], value["label"]) - - return p_gen - This results in the following set of parameters, matching the lulesh sample workflow: .. table:: Sample parameters from itertools_pgen.py diff --git a/samples/parameterization/itertools_pgen.py b/samples/parameterization/lulesh_itertools_pgen.py similarity index 100% rename from samples/parameterization/itertools_pgen.py rename to samples/parameterization/lulesh_itertools_pgen.py From 83942467667d29470ccf6794771da115b4e6e873 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 26 May 2020 14:51:01 -0700 Subject: [PATCH 12/15] Update doc strings on parameter generator samples --- samples/parameterization/custom_generator.py | 4 +++- samples/parameterization/lulesh_custom_gen.py | 4 ++-- .../parameterization/lulesh_itertools_pgen.py | 10 +++++++++ .../lulesh_montecarlo_args.py | 9 +++++--- .../parameterization/np_cheb_pgen_pargs.py | 21 +++++++++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/samples/parameterization/custom_generator.py b/samples/parameterization/custom_generator.py index 1b504c791..87e031656 100644 --- a/samples/parameterization/custom_generator.py +++ b/samples/parameterization/custom_generator.py @@ -5,8 +5,10 @@ def get_custom_generator(env, **kwargs): """ - Create a custom populated ParameterGenerator. + Create a custom populated ParameterGenerator with a single Parameter. + :params env: A StudyEnvironment object containing custom information. + :params kwargs: A dictionary of keyword arguments this function uses. :returns: A ParameterGenerator populated with parameters. """ p_gen = ParameterGenerator() diff --git a/samples/parameterization/lulesh_custom_gen.py b/samples/parameterization/lulesh_custom_gen.py index 3dbb95352..9c9706efc 100644 --- a/samples/parameterization/lulesh_custom_gen.py +++ b/samples/parameterization/lulesh_custom_gen.py @@ -6,11 +6,11 @@ def get_custom_generator(env, **kwargs): """ Create a custom populated ParameterGenerator. - This function recreates the exact same parameter set as the sample LULESH specifications. The point of this file is to present an example of how to generate custom parameters. - + :params env: A StudyEnvironment object containing custom information. + :params kwargs: A dictionary of keyword arguments this function uses. :returns: A ParameterGenerator populated with parameters. """ p_gen = ParameterGenerator() diff --git a/samples/parameterization/lulesh_itertools_pgen.py b/samples/parameterization/lulesh_itertools_pgen.py index 58c7495f1..6fe706962 100644 --- a/samples/parameterization/lulesh_itertools_pgen.py +++ b/samples/parameterization/lulesh_itertools_pgen.py @@ -3,6 +3,16 @@ def get_custom_generator(env, **kwargs): + """ + Create a custom populated ParameterGenerator. + This function recreates the exact same parameter set as the sample LULESH + specifications. The difference here is that itertools is employed to + programatically generate the samples instead of manually writing out + all of the combinations. + :params env: A StudyEnvironment object containing custom information. + :params kwargs: A dictionary of keyword arguments this function uses. + :returns: A ParameterGenerator populated with parameters. + """ p_gen = ParameterGenerator() sizes = (10, 20, 30) diff --git a/samples/parameterization/lulesh_montecarlo_args.py b/samples/parameterization/lulesh_montecarlo_args.py index 574495f16..7e3cd9fb4 100644 --- a/samples/parameterization/lulesh_montecarlo_args.py +++ b/samples/parameterization/lulesh_montecarlo_args.py @@ -8,9 +8,12 @@ def get_custom_generator(env, **kwargs): """ Create a custom populated ParameterGenerator. - This function recreates the exact same parameter set as the sample LULESH - specifications. The point of this file is to present an example of how to - generate custom parameters. + This function adapts the LULESH custom generator to randomly generate + values for the SIZE parameter within a prescribed range. An optional + seed is included, which if present on the command line or in the spec's + env block will allow reproducible random values. + + :params env: A StudyEnvironment object containing custom information. :params kwargs: A dictionary of keyword arguments this function uses. :returns: A ParameterGenerator populated with parameters. """ diff --git a/samples/parameterization/np_cheb_pgen_pargs.py b/samples/parameterization/np_cheb_pgen_pargs.py index 21f83bd09..7319d7f47 100644 --- a/samples/parameterization/np_cheb_pgen_pargs.py +++ b/samples/parameterization/np_cheb_pgen_pargs.py @@ -3,6 +3,14 @@ def chebyshev_dist(var_range, num_pts): + """ + Helper function for generating Chebyshev points in a specified range. + + :params var_range: Length 2 list or tuple defining the value range + :params num_pts: Integer number of points to generate + :returns: ndarrays of the Chebyshev x points, and the corresponding y + values of the circular mapping + """ r = 0.5*(var_range[1] - var_range[0]) angles = np.linspace(np.pi, 0.0, num_pts) @@ -13,6 +21,19 @@ def chebyshev_dist(var_range, num_pts): def get_custom_generator(env, **kwargs): + """ + Create a custom populated ParameterGenerator. + This function generates a 1D distribution of points for a single variable, + using the Chebyshev points scaled to the requested range. + The point of this file is to present an example of using external libraries + and helper functions to generate parameter value distributions. This + technique can be used to build reusable/modular sampling libraries that + pgen can hook into. + + :params env: A StudyEnvironment object containing custom information. + :params kwargs: A dictionary of keyword arguments this function uses. + :returns: A ParameterGenerator populated with parameters. + """ p_gen = ParameterGenerator() # Unpack any pargs passed in From f6ea32e8974939e6729e9fc476eb14d511203e4e Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 26 May 2020 14:57:42 -0700 Subject: [PATCH 13/15] Make flake8 happy --- samples/parameterization/lulesh_itertools_pgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/parameterization/lulesh_itertools_pgen.py b/samples/parameterization/lulesh_itertools_pgen.py index 6fe706962..a7eaa1170 100644 --- a/samples/parameterization/lulesh_itertools_pgen.py +++ b/samples/parameterization/lulesh_itertools_pgen.py @@ -6,7 +6,7 @@ def get_custom_generator(env, **kwargs): """ Create a custom populated ParameterGenerator. This function recreates the exact same parameter set as the sample LULESH - specifications. The difference here is that itertools is employed to + specifications. The difference here is that itertools is employed to programatically generate the samples instead of manually writing out all of the combinations. :params env: A StudyEnvironment object containing custom information. From 43199b5c9bd9ca0d0025efa30b4c1e23e807152f Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 26 May 2020 16:12:08 -0700 Subject: [PATCH 14/15] Misc cleanup and formatting, adding more links and internal references --- docs/source/parameters.rst | 72 ++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index dd8c0cf98..cf265ac0d 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -1,12 +1,13 @@ -Specifying Study Parameters -=========================== +============================= + Specifying Study Parameters +============================= -Maestro supports parameterization as a means of iterating over steps in a study with varying information. Maestro uses token replacement to define variables in the study specification to be replaced when executing the study. Token replacement can be used in various context in Maestro ; however Maestro implements specific features for managing parameters. +Maestro supports parameterization as a means of iterating over steps in a study with varying information. Maestro uses token replacement to define variables in the study specification to be replaced when executing the study. Token replacement can be used in various contexts in Maestro; however Maestro implements specific features for managing parameters. .. note add (as described in Maestro Token Replacement) reference before the ; when section is added -Maestro makes no assumptions about how parameters are defined or used in a study; this makes Maestro very flexible in regard to how a study may be configured with parameters. +Maestro makes no assumptions about how parameters are defined or used in a study, enabling flexibility in regards to how a study may be configured with parameters. There are two ways Maestro supports parameters: @@ -19,9 +20,9 @@ There are two ways Maestro supports parameters: similar for the token replacement sections: hello_world should be the quick start, not the exhaustive discussion Maestro Parameter Block -*********************** +======================= -The quickest and easiest way to setup parameters in a Maestro study is by defining a the global.parameters block directly in the specification +The quickest and easiest way to setup parameters in a Maestro study is by defining a **global.parameters** block directly in the specification .. code-block:: yaml :linenos: @@ -39,7 +40,7 @@ The quickest and easiest way to setup parameters in a Maestro study is by defini The above example defines the parameters TRIAL, SIZE, and ITERATIONS. Parameters can be used in study steps to vary information. When a parameter is defined in a study, Maestro will automatically detect the usage of a parameter moniker and handle the substitution automatically in the study expansion. This ensures that each set of parameters are run as part of the study. -The `label` key in the block specifies the pattern to use for the directory name when the workspace is created. By default, Maestro constructs a unique workspace for each parameter combination. +The **label** key in the block specifies the pattern to use for the directory name when the workspace is created. By default, Maestro constructs a unique workspace for each parameter combination. Defining multiple parameters in the parameter block will share a 1:1 mapping. Maestro requires all combinations be resolved when using the parameter block. The combinations in the above example will be expanded as follows: @@ -71,13 +72,26 @@ Even though this is defined in Maestro as a single step, Maestro will automatica Maestro will only use parameters if they've been defined in at least one step +In addition to direct access to parameter values, parameter labels can also be used in steps: + +.. code-block:: yaml + :linenos: + + - name: run-lulesh + description: Run LULESH. + run: + cmd: | + echo "Running case: $(SIZE.label), $(ITERATIONS.label)" + $(LULESH)/lulesh2.0 -s $(SIZE) -i $(ITERATIONS) -p > $(outfile) + depends: [make-lulesh] + .. note add example with using the label in steps too What can be Parameterized in Maestro? -************************************* +===================================== -A common use case for Maestro is to use the parameter block to specify samples to iterate over for a simulation parameter study; however, Maestro does not make any assumptions about this use case. This makes the use of Maestro's parameter block very flexible. For example, Maestro does not require the parameter variations to be numeric. +A common use case for Maestro is to use the parameter block to specify values to iterate over for a simulation parameter study; however, Maestro does not make any assumptions about what these values are. This makes the use of Maestro's parameter block very flexible. For example, Maestro does not require the parameter variations to be numeric. .. code-block:: yaml :linenos: @@ -96,7 +110,7 @@ A common use case for Maestro is to use the parameter block to specify samples t values : [input1.in, input2.in, input3.in] label : INPUT.%% -The above example highlights a partial study spec that defines a parameter block of simulation inputs that will be varied when the study runs. The ``run-simulation`` will run three times, once for each defined input file. +The above example highlights a partial study spec that defines a parameter block of simulation inputs that will be varied when the study runs. The **run-simulation** step will run three times, once for each defined input file. .. code-block:: yaml :linenos: @@ -118,10 +132,10 @@ The above example highlights a partial study spec that defines a parameter block values : [4.0.0, 4.0.0, 4.0.0, 5.0.0, 5.0.0, 5.0.0] label : VERSION.%% -This example parameterizes the inputs and the version of the code being run. Maestro will run each input with the different code version. The above example assumes that all the code versions share a base path, ``$(CODE_PATH)`` which is inserted via the token replacment mechanism to yeild the full paths (e.g. /usr/gapps/code/4.0.0/code.exe). +This example parameterizes the inputs and the version of the code being run. Maestro will run each input with the different code version. The above example assumes that all the code versions share a base path, **$(CODE_PATH)** which is inserted via the token replacment mechanism to yeild the full paths (e.g. /usr/gapps/code/4.0.0/code.exe). Where can Parameters be used in Study Steps? -******************************************** +============================================ Maestro uses monikers to reference parameters in study steps, and will automatically perform token replacement on used parameters when the study is run. The page Maestro Token Replacement goes into detail about how token replacement works in Maestro. @@ -133,7 +147,7 @@ Maestro is very flexible in the way it manages token replacement for parameters Cmd block --------- -Parameters can be defined in the Maestro `cmd` block in the study step. Everything in Maestro's `cmd` block will be written to a bash shell or batch script (if batch is configured). Any shell commands should be valid in the `cmd` block. A common way to use parameters is to pass them in via arguments to a code, script, or tool. +Parameters can be defined in the Maestro **cmd** block in the study step. Everything in Maestro's **cmd** block will be written to a bash shell or batch script (if batch is configured). Any shell commands should be valid in the **cmd** block. A common way to use parameters is to pass them in via arguments to a code, script, or tool. .. code-block:: yaml :linenos: @@ -187,20 +201,18 @@ Step based batch configurations can also be parameterized in Maestro. This provi label : PROC.%% .. note - Make the sub sections appear as subsections for the relevent blocks (cmd block, batch configurations..) - Add some dag graphs in here at some point? Parameter Generator (pgen) -************************** +========================== -Maestro's Parameter Generator (pgen) supports setting up more flexible and complex parameter generation. Maestro's pgen is a user supplied python file that contains the parameter generation logic, overriding the global.parameters block in the yaml specification file. To run a Maestro study using a parameter generator just pass in the pgen file to Maestro on the command line when launching the study: +Maestro's Parameter Generator (pgen) supports setting up more flexible and complex parameter generation. Maestro's pgen is a user supplied python file that contains the parameter generation logic, overriding the **global.parameters** block in the yaml specification file. To run a Maestro study using a parameter generator just pass in the path to the pgen file to Maestro on the command line when launching the study, such as this example where the study specification file and pgen file live in the same directory: .. code-block:: bash $ maestro run study.yaml --pgen pgen.py -The minimum requirements for making a valid pgen file is to make a function called ``get_custom_generator`` which returns a Maestro :py:class:`~maestrowf.datastructures.core.ParameterGenerator` object as demonstrated in the simple example below: +The minimum requirements for making a valid pgen file is to make a function called **get_custom_generator** which returns a Maestro :py:class:`~maestrowf.datastructures.core.ParameterGenerator` object as demonstrated in the simple example below: .. code-block:: python :linenos: @@ -222,16 +234,17 @@ The minimum requirements for making a valid pgen file is to make a function call return p_gen -The object simply builds the same nested key:value pairs seen in the global.parameters block available in the yaml specification. +The object simply builds the same nested key:value pairs seen in the **global.parameters** block available in the yaml specification. For this simple example above, this may not offer compelling advantages over writing out the flattened list in the yaml specification directly. This programmatic approach becomes preferable when expanding studies to use hundreds of parameters and parameter values or requiring non-trivial parameter value distributions. The following examples will demonstrate these scenarios using both standard python library tools and additional 3rd party packages from the larger python ecosystem. EXAMPLE: - Using Python's standard ``itertools`` package to perform a Cartesian Product of parameters in the lulesh example specification. + Using Python's standard `itertools `_ package to perform a Cartesian Product of parameters in the lulesh example specification. .. literalinclude:: ../../samples/parameterization/lulesh_itertools_pgen.py :language: python :caption: lulesh_itertools_pgen.py + :name: lulesh_itertools_pgen :linenos: This results in the following set of parameters, matching the lulesh sample workflow: @@ -248,17 +261,19 @@ This results in the following set of parameters, matching the lulesh sample work ITER 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== +.. _pargs_section: + Pgen Arguments (pargs) -********************** +====================== -There is an additional pgen feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the itertools_pgen.py file to change that. Maestro supports passing arguments to these generator functions on the command line: +There is an additional pgen feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the :ref:`lulesh_itertools_pgen` file to change that. Maestro supports passing arguments to these generator functions on the command line: .. code-block:: bash $ maestro run study.yaml --pgen itertools_pgen_pargs.py --parg "SIZE_MIN:10" --parg "SIZE_STEP:10" --parg "NUM_SIZES:4" -Each argument is a string in ``key:value`` form, which can be accessed in the generator function as shown below: +Each argument is a string in **key:value** form, which can be accessed in the parameter generator function as shown below: .. code-block:: python :name: itertools_pgen_pargs.py @@ -324,7 +339,7 @@ Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields t Notice that using the pgen input method makes it trivially easy to add 1000's of parameters, something which would be cumbersome via manual editing of the global.parameters block in the study specification file. -The next example demonstrates using 3rd party libraries and breaking out the actual parameter generation algorithm into separate helper functions that the ``get_custom_generator`` function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. There are no requirements to cram all of the logic into the ``get_custom_generator`` function, so we'll build the sampling logic into a helper function instead. The simple parameter distribution demoed in this example is for single variables and is encountered in polynomial interpolation applications and is designed to suppress the Runge and Gibbs phenomena by sampling the function at the Chebyshev nodes. +There are no requirements to cram all of the logic into the **get_custom_generator** function. The next example demonstrates using 3rd party libraries and breaking out the actual parameter generation algorithm into separate helper functions that the **get_custom_generator** function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. The simple parameter distribution demoed in here is for single variables and is often encountered in polynomial interpolation applications and is designed to suppress the Runge phenomena by sampling the function to be interpolated at the Chebyshev nodes. EXAMPLE: Using ``numpy`` to calculate a sampling of a function at the Chebyshev nodes. @@ -341,19 +356,16 @@ Running this parameter generator with the following pargs $ maestro run study.yaml --pgen np_cheb_pgen.py --parg "X_MIN:0" --parg "X_MAX:3" --parg "NUM_PTS:11" -results in the 1D distribution of points for the ``X`` parameter shown by the orange circles: +results in the 1D distribution of points for the **X** parameter shown by the orange circles: .. image:: pgen_images/cheb_map.png Referencing Values from a Specification's Env Block -*************************************************** +=================================================== -In addition to command line arguments via ``pargs``, the variables defined in the env block in the workflow specification file can be accessed inside the ParameterGenerator objects, which is passed in to ``get_custom_generator`` as the first argument. The lulesh sample specification can be extended to store the default values for the pgen, enhancing the reproducability of the generator. The following example extends the lulesh_monte_carlo_args.py sample generator and adds one additional env var, `seed`, which can be overriden via ``pargs``, making the default configuration a fully repeatable study specification. The variables are accessed via the :py:class:`~maestrowf.datastructures.core.StudyEnvironment`'s :py:func:`~maestrowf.datastructures.core.StudyEnvironment.find()` function, which will return ``None`` if the variable is not defined in the study specification. +In addition to command line arguments via :ref:`pargs_section`, the variables defined in the **env** block in the workflow specification file can be accessed inside the :py:class:`~maestrowf.datastructures.core.ParameterGenerator` objects, which is passed in to **get_custom_generator** as the first argument. The lulesh sample specification can be extended to store the default values for the **pgen**, enhancing the reproducability of the generator. The following example makes use of an optional **seed** parameter which can be added to the **env** block or set via **pargs** to make a repeatable study specification, while omitting it can enable fully randomized workflows upon every instantiation. The variables are accessed via the :py:class:`~maestrowf.datastructures.core.StudyEnvironment`'s :py:func:`~maestrowf.datastructures.core.StudyEnvironment.find()` function, which will return ``None`` if the variable is not defined in the study specification. -.. note - possible to link the sample files directly? -> also, might be good to have them pulled into the docs automatically - somewhere.. .. literalinclude:: ../../samples/parameterization/lulesh_montecarlo_args.py :language: python From 0f97a42d65c7bc6cd20411952a077aa7d7dbda02 Mon Sep 17 00:00:00 2001 From: Jeremy White Date: Tue, 26 May 2020 22:38:08 -0700 Subject: [PATCH 15/15] Fix up section listing, in-text moniker/function formatting --- docs/source/parameters.rst | 50 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/source/parameters.rst b/docs/source/parameters.rst index cf265ac0d..4ca6f2553 100644 --- a/docs/source/parameters.rst +++ b/docs/source/parameters.rst @@ -11,9 +11,9 @@ Maestro makes no assumptions about how parameters are defined or used in a study There are two ways Maestro supports parameters: - * Directly in the study specification as the global.parameters block + * Directly in the study specification as the ``global.parameters`` block - * Through the use of a user created Python module called Parameter Generator (pgen) + * Through the use of a user created Python module called :ref:`pgen_section` .. note add reference to this section from the hello_world examples (ref here for more thorough discussion of parameters) @@ -22,7 +22,7 @@ There are two ways Maestro supports parameters: Maestro Parameter Block ======================= -The quickest and easiest way to setup parameters in a Maestro study is by defining a **global.parameters** block directly in the specification +The quickest and easiest way to setup parameters in a Maestro study is by defining a ``global.parameters`` block directly in the specification .. code-block:: yaml :linenos: @@ -38,9 +38,9 @@ The quickest and easiest way to setup parameters in a Maestro study is by defini values : [10, 20, 30, 10, 20, 30, 10, 20, 30] label : ITER.%% -The above example defines the parameters TRIAL, SIZE, and ITERATIONS. Parameters can be used in study steps to vary information. When a parameter is defined in a study, Maestro will automatically detect the usage of a parameter moniker and handle the substitution automatically in the study expansion. This ensures that each set of parameters are run as part of the study. +The above example defines the parameters ``TRIAL``, ``SIZE``, and ``ITERATIONS``. Parameters can be used in study steps to vary information. When a parameter is defined in a study, Maestro will automatically detect the usage of a parameter moniker and handle the substitution automatically in the study expansion. This ensures that each set of parameters are run as part of the study. -The **label** key in the block specifies the pattern to use for the directory name when the workspace is created. By default, Maestro constructs a unique workspace for each parameter combination. +The ``label`` key in the block specifies the pattern to use for the directory name when the workspace is created. By default, Maestro constructs a unique workspace for each parameter combination. Defining multiple parameters in the parameter block will share a 1:1 mapping. Maestro requires all combinations be resolved when using the parameter block. The combinations in the above example will be expanded as follows: @@ -52,7 +52,7 @@ Defining multiple parameters in the parameter block will share a 1:1 mapping. Ma * ... -Maestro does not do any additional operations on parameters such as cross products. If more complex methodologies are required to define parameters then the use of Maestro's Parameter Generator (pgen) is recommended. +Maestro does not do any additional operations on parameters such as cross products. If more complex methodologies are required to define parameters then the use of Maestro's :ref:`pgen_section` is recommended. Defined parameters can be used in steps directly: @@ -72,7 +72,7 @@ Even though this is defined in Maestro as a single step, Maestro will automatica Maestro will only use parameters if they've been defined in at least one step -In addition to direct access to parameter values, parameter labels can also be used in steps: +In addition to direct access to parameter values, a parameter label can be used in steps by appending the ``.label`` moniker to the name (as seen below with ``$(ITERATIONS.label)``): .. code-block:: yaml :linenos: @@ -85,8 +85,6 @@ In addition to direct access to parameter values, parameter labels can also be u $(LULESH)/lulesh2.0 -s $(SIZE) -i $(ITERATIONS) -p > $(outfile) depends: [make-lulesh] -.. note - add example with using the label in steps too What can be Parameterized in Maestro? ===================================== @@ -110,7 +108,7 @@ A common use case for Maestro is to use the parameter block to specify values to values : [input1.in, input2.in, input3.in] label : INPUT.%% -The above example highlights a partial study spec that defines a parameter block of simulation inputs that will be varied when the study runs. The **run-simulation** step will run three times, once for each defined input file. +The above example highlights a partial study spec that defines a parameter block of simulation inputs that will be varied when the study runs. The ``run-simulation`` step will run three times, once for each defined input file. .. code-block:: yaml :linenos: @@ -132,7 +130,7 @@ The above example highlights a partial study spec that defines a parameter block values : [4.0.0, 4.0.0, 4.0.0, 5.0.0, 5.0.0, 5.0.0] label : VERSION.%% -This example parameterizes the inputs and the version of the code being run. Maestro will run each input with the different code version. The above example assumes that all the code versions share a base path, **$(CODE_PATH)** which is inserted via the token replacment mechanism to yeild the full paths (e.g. /usr/gapps/code/4.0.0/code.exe). +This example parameterizes the inputs and the version of the code being run. Maestro will run each input with the different code version. The above example assumes that all the code versions share a base path, ``$(CODE_PATH)`` which is inserted via the token replacment mechanism to yeild the full paths (e.g. /usr/gapps/code/4.0.0/code.exe). Where can Parameters be used in Study Steps? ============================================ @@ -147,7 +145,7 @@ Maestro is very flexible in the way it manages token replacement for parameters Cmd block --------- -Parameters can be defined in the Maestro **cmd** block in the study step. Everything in Maestro's **cmd** block will be written to a bash shell or batch script (if batch is configured). Any shell commands should be valid in the **cmd** block. A common way to use parameters is to pass them in via arguments to a code, script, or tool. +Parameters can be defined in the Maestro ``cmd`` block in the study step. Everything in Maestro's ``cmd`` block will be written to a bash shell or batch script (if batch is configured). Any shell commands should be valid in the ``cmd`` block. A common way to use parameters is to pass them in via arguments to a code, script, or tool. .. code-block:: yaml :linenos: @@ -203,16 +201,18 @@ Step based batch configurations can also be parameterized in Maestro. This provi .. note Add some dag graphs in here at some point? +.. _pgen_section: + Parameter Generator (pgen) ========================== -Maestro's Parameter Generator (pgen) supports setting up more flexible and complex parameter generation. Maestro's pgen is a user supplied python file that contains the parameter generation logic, overriding the **global.parameters** block in the yaml specification file. To run a Maestro study using a parameter generator just pass in the path to the pgen file to Maestro on the command line when launching the study, such as this example where the study specification file and pgen file live in the same directory: +Maestro's Parameter Generator (**pgen**) supports setting up more flexible and complex parameter generation. Maestro's pgen is a user supplied python file that contains the parameter generation logic, overriding the ``global.parameters`` block in the yaml specification file. To run a Maestro study using a parameter generator just pass in the path to the **pgen** file to Maestro on the command line when launching the study, such as this example where the study specification file and **pgen** file live in the same directory: .. code-block:: bash $ maestro run study.yaml --pgen pgen.py -The minimum requirements for making a valid pgen file is to make a function called **get_custom_generator** which returns a Maestro :py:class:`~maestrowf.datastructures.core.ParameterGenerator` object as demonstrated in the simple example below: +The minimum requirements for making a valid pgen file is to make a function called :py:func:`get_custom_generator` which returns a Maestro :py:class:`~maestrowf.datastructures.core.ParameterGenerator` object as demonstrated in the simple example below: .. code-block:: python :linenos: @@ -234,7 +234,7 @@ The minimum requirements for making a valid pgen file is to make a function call return p_gen -The object simply builds the same nested key:value pairs seen in the **global.parameters** block available in the yaml specification. +The object simply builds the same nested key:value pairs seen in the ``global.parameters`` block available in the yaml specification. For this simple example above, this may not offer compelling advantages over writing out the flattened list in the yaml specification directly. This programmatic approach becomes preferable when expanding studies to use hundreds of parameters and parameter values or requiring non-trivial parameter value distributions. The following examples will demonstrate these scenarios using both standard python library tools and additional 3rd party packages from the larger python ecosystem. @@ -264,16 +264,16 @@ This results in the following set of parameters, matching the lulesh sample work .. _pargs_section: Pgen Arguments (pargs) -====================== +---------------------- -There is an additional pgen feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the :ref:`lulesh_itertools_pgen` file to change that. Maestro supports passing arguments to these generator functions on the command line: +There is an additional :ref:`pgen ` feature that can be used to make them more dynamic. The above example generates a fixed set of parameters, requiring editing the :ref:`lulesh_itertools_pgen` file to change that. Maestro supports passing arguments to these generator functions on the command line: .. code-block:: bash $ maestro run study.yaml --pgen itertools_pgen_pargs.py --parg "SIZE_MIN:10" --parg "SIZE_STEP:10" --parg "NUM_SIZES:4" -Each argument is a string in **key:value** form, which can be accessed in the parameter generator function as shown below: +Each argument is a string in ``key:value`` form, which can be accessed in the parameter generator function as shown below: .. code-block:: python :name: itertools_pgen_pargs.py @@ -323,7 +323,7 @@ Each argument is a string in **key:value** form, which can be accessed in the pa return p_gen -Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields the expanded parameter set: +Passing the **pargs** ```SIZE_MIN:10'``, ``'SIZE_STEP:10'``, and ``'NUM_SIZES:4'`` then yields the expanded parameter set: .. table:: Sample parameters from itertools_pgen_pargs.py @@ -337,12 +337,12 @@ Passing the pargs 'SIZE_MIN:10', 'SIZE_STEP:10', and 'NUM_SIZES:4' then yields t ITER 10 20 30 10 20 30 10 20 30 10 20 30 =========== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== -Notice that using the pgen input method makes it trivially easy to add 1000's of parameters, something which would be cumbersome via manual editing of the global.parameters block in the study specification file. +Notice that using the pgen input method makes it trivially easy to add 1000's of parameters, something which would be cumbersome via manual editing of the ``global.parameters`` block in the study specification file. -There are no requirements to cram all of the logic into the **get_custom_generator** function. The next example demonstrates using 3rd party libraries and breaking out the actual parameter generation algorithm into separate helper functions that the **get_custom_generator** function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the maestro executable you are using. The simple parameter distribution demoed in here is for single variables and is often encountered in polynomial interpolation applications and is designed to suppress the Runge phenomena by sampling the function to be interpolated at the Chebyshev nodes. +There are no requirements to cram all of the logic into the :py:func:`get_custom_generator` function. The next example demonstrates using 3rd party libraries and breaking out the actual parameter generation algorithm into separate helper functions that the :py:func:`get_custom_generator` function uses to get some more complicated distributions. The only concerns with this approach will be to ensure the library is installed in the same virtual environment as the Maestro executable you are using. The simple parameter distribution demoed in here is often encountered in polynomial interpolation applications and is designed to suppress the Runge phenomena by sampling the function to be interpolated at the Chebyshev nodes. EXAMPLE: - Using ``numpy`` to calculate a sampling of a function at the Chebyshev nodes. + Using `numpy `_ to calculate a sampling of a function at the Chebyshev nodes. .. literalinclude:: ../../samples/parameterization/np_cheb_pgen_pargs.py :language: python @@ -356,15 +356,15 @@ Running this parameter generator with the following pargs $ maestro run study.yaml --pgen np_cheb_pgen.py --parg "X_MIN:0" --parg "X_MAX:3" --parg "NUM_PTS:11" -results in the 1D distribution of points for the **X** parameter shown by the orange circles: +results in the 1D distribution of points for the ``X`` parameter shown by the orange circles: .. image:: pgen_images/cheb_map.png Referencing Values from a Specification's Env Block -=================================================== +--------------------------------------------------- -In addition to command line arguments via :ref:`pargs_section`, the variables defined in the **env** block in the workflow specification file can be accessed inside the :py:class:`~maestrowf.datastructures.core.ParameterGenerator` objects, which is passed in to **get_custom_generator** as the first argument. The lulesh sample specification can be extended to store the default values for the **pgen**, enhancing the reproducability of the generator. The following example makes use of an optional **seed** parameter which can be added to the **env** block or set via **pargs** to make a repeatable study specification, while omitting it can enable fully randomized workflows upon every instantiation. The variables are accessed via the :py:class:`~maestrowf.datastructures.core.StudyEnvironment`'s :py:func:`~maestrowf.datastructures.core.StudyEnvironment.find()` function, which will return ``None`` if the variable is not defined in the study specification. +In addition to command line arguments via :ref:`pargs `, the variables defined in the ``env`` block in the workflow specification file can be accessed inside the :py:class:`~maestrowf.datastructures.core.ParameterGenerator` objects, which is passed in to :py:func:`get_custom_generator` as the first argument. The lulesh sample specification can be extended to store the default values for the :ref:`pgen `, enhancing the reproducability of the generator. The following example makes use of an optional ``seed`` parameter which can be added to the ``env`` block or set via :ref:`pargs ` to make a repeatable study specification, while omitting it can enable fully randomized workflows upon every instantiation. The variables are accessed via the :py:class:`~maestrowf.datastructures.core.StudyEnvironment`'s :py:func:`~maestrowf.datastructures.core.StudyEnvironment.find()` function, which will return ``None`` if the variable is not defined in the study specification. .. literalinclude:: ../../samples/parameterization/lulesh_montecarlo_args.py