Tutorial: Writing Builders#
This tutorial will introduce WAVES style builders using the
waves.scons_extensions.first_target_builder_factory()
with considerations for handling some representative types
of numeric solver behavior. For an example of writing more generic SCons Builders, see the SCons user manual
[32]. For a quickstart with simple builders that mimic the WAVES builder directory change operations,
see the SCons Quickstart.
References#
Environment#
SCons and WAVES can be installed in a Conda environment with the Conda package manager. See the Conda installation and Conda environment management documentation for more details about using Conda.
Note
The SALib and numpy versions may not need to be this strict for most tutorials. However, Tutorial: Sensitivity Study uncovered some undocumented SALib version sensitivity to numpy surrounding the numpy v2 rollout.
Create the tutorials environment if it doesn’t exist
$ conda create --name waves-tutorial-env --channel conda-forge waves 'scons>=4.6' matplotlib pandas pyyaml xarray seaborn 'numpy>=2' 'salib>=1.5.1' pytest
PS > conda create --name waves-tutorial-env --channel conda-forge waves scons matplotlib pandas pyyaml xarray seaborn numpy salib pytest
Activate the environment
$ conda activate waves-tutorial-env
PS > conda activate waves-tutorial-env
Some tutorials require additional third-party software that is not available for the Conda package manager. This
software must be installed separately and either made available to SConstruct by modifying your system’s PATH
or by
modifying the SConstruct search paths provided to the waves.scons_extensions.add_program()
method.
Warning
STOP! Before continuing, check that the documentation version matches your installed package version.
You can find the documentation version in the upper-left corner of the webpage.
You can find the installed WAVES version with
waves --version
.
If they don’t match, you can launch identically matched documentation with the WAVES Command-Line Utility
docs subcommand as waves docs
.
Directory Structure#
Create a new
tutorial_writing_builders
directory with thewaves fetch
command below
$ pwd
/home/roppenheimer/waves-tutorials
$ waves fetch --destination tutorial_writing_builders tutorials/tutorial_writing_builders
$ ls tutorial_writing_builders
SConstruct implicit.yaml implicit_workflow pyproject.toml scons_extensions.py solver test_scons_extensions.py
Make the new
tutorial_writing_builders
directory the current working directory
$ pwd
/home/roppenheimer/waves-tutorials
$ cd tutorial_writing_builders
$ pwd
/home/roppenheimer/waves-tutorials/tutorial_writing_builders
$ ls tutorial_writing_builders
SConstruct implicit.yaml implicit_workflow pyproject.toml scons_extensions.py solver test_scons_extensions.py
(Linux and MacOS only) Make the
solver.py
file executable. In a shell, use the command$ chmod +x solver.py
Solver#
Before writing a builder, it is important to understand the solver’s file handling behaviors, exit behaviors, and command line interface options. The specific nature of the numeric algorithms and solution procedures are important considerations when choosing a solver, but when writing a builder only the interfaces with the file system and shell are important. To write a robust builder, you need four things from the solver
Deterministic output file paths at workflow configure time
Well defined exit criteria
Command line options for non-interactive behavior control
Non-interactive output file overwrite control options
It is also helpful for the solver to provide
Complete control over all output file paths and file base names, including logs
Deterministic output file paths are necessary to allow builder users to define targets for downstream task consumption.
While it is possible to write a builder based on waves.scons_extensions.first_target_emitter()
that does not track
any solver owned output files, such a builder is difficult to robustly integrate in a workflow. If solver output file
paths are non-deterministic at workflow configure time or difficult to control from the CLI, it is still possible to
write a builder by tracking the STDOUT and STDERR messages and redirecting them to a task-specific *.stdout
file.
However, this pushes more effort of workflow configuration onto end users of the builder. If all or some output file
paths can be determined at configure time it is possible to write an associated SCons Emitters to automatically
populate known builder targets.
Well defined exit criteria is necessary to return reliable task states on solver failures and successes. Most builders should avoid modifying the default solver behavior to provide users with the documented, and therfore expected, behavior. However, when solvers under report non-zero exit codes for fatal errors, false positives, it may be necessary to do extra work, such as log file inspection, to return more reliable exit codes and avoid attempting downstream tasks when the solver task is known to have failed. This is especially important when the solver writes partial output results that may propagate false positives further down the workflow. False negatives can be equally frustrating when the solver results files are sufficiently complete for the user’s workflow and should not be considered fatal. Because desirable task outcome behavior is often project or even workflow specific, it is usually best to maintain the documented solver behavior when writing builders for general distribution.
Command line options for non-interactive behavior control are necessary to provide automatic execution of large
workflows and parametric studies. A large benefit of using build systems is providing consistent, reproducible behavior.
This is much more difficult when when interactive controls allow workflow behavioral changes. Additionally, interactive
controls do not scale well. Finally, when using the waves.scons_extensions.first_target_builder_factory()
and
associated waves.scons_extensions.first_target_emitter()
, the solver STDOUT and STDERR are redirected to a
task-specific log file to provide at least one target file and make task-specific trouble-shooting less cumbersome. It
is possible to modify the builder factory’s redirection operation to simultaneously stream to STDOUT/STDERR and a
redirected task log file; however, the commands implementing such behavior are not common to all operating systems and
shells and is therefore best left to the end user and project specific needs. As a default, a less cluttered STDOUT is
often more useful for end users tracking workflow execution, too.
Output overwrite controls are important when writing builders. If the solver increments output file names, workflow re-execution will create file system state dependent targets. Technically these may be deterministic at configure time. However, as a practical matter these are difficult to predict and functionally non-deterministic from the perspective of the builder author because they may depend on the end user’s task construction. If the solver provides an option to overwrite a deterministic output file name, the builder and any associated emitter are much easier to write and test for completeness.
If the output files are controllable and deterministic, then it is possible to work around a missing overwrite feature
by adding a pre-solver action that performs file system cleanup as part of the builder. Writing a multi-action builder
is outside the scope of the current tutorial, but there are examples in SCons Multi-Action Tasks and the
waves.scons_extensions.action_list_strings()
and waves.scons_extensions.action_list_scons()
functions can
help advanced users pull apart existing builders to augment the action list.
The placeholder solver used in this tutorial does no real work, but provides some common output handling use cases as examples for considerations in builder design. As seen in the solver documentation below, the solver writes a log file with no path controls and no overwrite behavior. The solver builder can not reliably manage this file, except to change to the target parent directory prior to solver execution to ensure that the log is co-located with other task output files. The output file names are deterministic, but require task specific knowledge of the solver options. As a first draft, the builder will make no attempt to predict output file names and instead rely on the user’s knowledge of solver behavior and task options to write complete target lists. There is an overwrite option, which the builder will require to make task behavior more predictable. There is no interactive behavior to work around and also no CLI option to force non-interactive behavior.
Normally the internal implementation and API are not accessible to solver users. Here, the full internal API is produced as an example of Python command line utility and to allow users to play with solver behavior to more closely match solvers of interest. While the Python implementation is written to WAVES style and functional programming best practices, the file handling behavior itself is undesirable when users have direct control over command line utility behavior.
Solver documentation#
Example for common commercial and research solver behavior and handling.
Warning
The solver I/O handling and CLI are NOT good behaviors for authors and developers of command line utilities. They are intended to be examples of representative types of challenging behaviors to consider when writing SCons builders.
Solver I/O behaviors:
Log files are written to the current working directory. Log files are never overwritten and must be cleaned manually. Log files are automatically incremented from
solver.log
tosolver.log10
. If no free log file number is found and the maximum number of 10 is found, the solver exits with a non-zero exit code.The output file name is built in preferred order: CLI
--output-file
argument or the--input-file
argument with the replacement extension.out
.The implicit and explicit routines write output file(s) based on the requested number of threads. If only one thread is requested, the output file name is used. If more than one thread, N, is requested, each thread writes to a separately numbered output file 0 to (N-1) as
output_file.out{number}
.If any output file exists and the overwrite behavior is not requested, the solver exits with a non-zero exit code.
Runtime errors are returned as non-zero exit codes. Internal errors are returned as the appropriate exception.
Exit codes:
error loading YAML input file
mismatched subcommand and input file routine request
output file exists and no overwrite was requested
reached max log file integer before finding a free file name
- solver.configure(args: Namespace) dict [source]
Return the configuration with appended executable information.
- Parameters:
args – The command line argument namespace
- Raises:
RuntimeError – if the subcommand doesn’t match the input file routine
- solver.explicit(args: Namespace) None [source]
Run the explicit ‘solve’ routine.
- Parameters:
args – The command line argument namespace
- solver.get_parser() ArgumentParser [source]
Return the argparse CLI parser.
- solver.implicit(args: Namespace) None [source]
Run the implicit ‘solve’ routine.
- Parameters:
args – The command line argument namespace
- solver.main() None [source]
Run the solver command line interface and program flow.
- solver.name_log_file(log_file: Path, max_iterations: int = 10) Path [source]
Return the first free log file name.
- Parameters:
log_file – Log file base name
max_iterations – Maximum number of allowable log files
- Raises:
RuntimeError – if no log file name is free within the max iterations
- solver.name_output_file(input_file: Path, output_file: Path) Path [source]
Create the output file name from the input file if not specified.
- solver.positive_nonzero_int(argument: str) int [source]
Type function for argparse - positive, non-zero integers.
- Parameters:
argument (str) – string argument from argparse
- Returns:
argument
- Return type:
int
- Raises:
ValueError –
The argument can’t be cast to int
The argument is less than 1
- solver.read_input(input_file: Path) dict [source]
Return the configuration by reading the input file and handling common errors.
- Parameters:
input_file – The input YAML file absolute or relative path
- Raises:
RuntimeError – if the YAML file can not be read
- solver.solve(configuration: dict) None [source]
Run the solver ‘simulation’ and create output files.
Common solve logic because we do not really have separate routines.
- Parameters:
configuration – The solver configuration
- Raises:
RuntimeError – if any output file already exists and overwrite is not requested.
- solver.solve_output_files(output_file: Path, solve_cpus: int) list[Path] [source]
Return the solve output file list to match the number of solve cpus.
- Parameters:
output_file – base name for the output file
solve_cpus – integer number of solve cpus
Builder#
The tutorial’s builder factory is written as a project specific Python module such that it could be documented and packaged for distribution. It is also possible to write builder factories and builders directly into SCons configuration files.
The builder itself is largely boilerplate documentation and a pass-through to the
waves.scons_extensions.first_target_builder_factory()
. Strictly speaking, the builder factory could be written
much more compactly by passing unchanged arguments through as *kwargs
. Hear, they are kept in the builder factory
definition to make documentation of the factory defaults easier and to explicitly set the defaults independently from
the WAVES function.
Writing the entire builder from scratch would not require many extra lines of code. The use of the WAVES function is primarily to provide consistency with WAVES builder factories. Builder factory authors are encouraged to read the SCons Builders documentation when their solver needs are not met by the WAVES template builder factories.
1"""Provide project specific extensions to SCons."""
2
3import collections
4import typing
5
6import SCons.Builder
7import waves
8
9
10# Write an example custom builder using the WAVES builder factory template
11def solver_builder_factory(
12 environment: str = "",
13 action_prefix: str = "cd ${TARGET.dir.abspath} &&",
14 program: str = "solver.py",
15 program_required: str = "",
16 program_options: str = "",
17 subcommand: str = "implicit",
18 subcommand_required: str = "--input-file ${SOURCES[0].abspath} --output-file=${TARGETS[0].abspath} --overwrite",
19 subcommand_options: str = "",
20 action_suffix: str = "> ${TARGETS[-1].abspath} 2>&1",
21 emitter: collections.abc.Callable[
22 [list, list, SCons.Environment.Environment], tuple[list, list]
23 ] = waves.scons_extensions.first_target_emitter,
24 **kwargs: dict[str, typing.Any],
25) -> SCons.Builder.Builder:
26 """Define the SCons builder for the solver module's command-line interface.
27
28 This builder factory extends :meth:`waves.scons_extensions.first_target_builder_factory`. This builder factory uses
29 the :meth:`waves.scons_extensions.first_target_emitter`. At least one task target must be specified in the task
30 definition and the last target will always be the expected STDOUT and STDERR redirection output file,
31 ``TARGETS[-1]`` ending in ``*.stdout``.
32
33 .. warning::
34
35 Users overriding the ``emitter`` keyword argument are responsible for providing an emitter with equivalent STDOUT
36 file handling behavior as :meth:`waves.scons_extensions.first_target_emitter` or updating the ``action_suffix``
37 option to match their emitter's behavior.
38
39 With the default options this builder requires the following sources file provided in the order:
40
41 1. solver program's routine subcommand input file: ``*.yaml``
42
43 .. code-block::
44 :caption: action string construction
45
46 ${environment} ${action_prefix} ${program} ${program_required} ${program_options} ${subcommand} ${subcommand_required} ${subcommand_options} ${action_suffix}
47
48 .. code-block::
49 :caption: action string default expansion
50
51 ${environment} cd ${TARGET.dir.abspath} && solver.py ${program_required} ${program_optional} implicit --input-file ${SOURCE.abspath} --output-file ${TARGETS[0].abspath} ${subcommand_options} > ${TARGETS[-1].abspath} 2>&1
52
53 .. code-block::
54 :caption: SConstruct
55
56 import waves
57 env = waves.scons_extensions.WAVESEnvironment()
58 env.AddMethod(waves.scons_extensions.add_program, "AddProgram")
59 env["solver"] = env.AddProgram(["solver"])
60 env.Append(BUILDERS={"Solver": solver_explicit_builder_factory()})
61 env.Solver(
62 target=["target.out"],
63 source=["source.yaml"],
64 )
65
66 The builder returned by this factory accepts all SCons Builder arguments. The arguments of this function are also
67 available as keyword arguments of the builder. When provided during task definition, the task keyword arguments
68 override the builder keyword arguments.
69
70 :param environment: This variable is intended primarily for use with builders and tasks that can not execute from an
71 SCons construction environment. For instance, when tasks execute on a remote server with SSH wrapped actions
72 using :meth:`waves.scons_extensions.ssh_builder_actions` and therefore must initialize the remote environment as
73 part of the builder action.
74 :param action_prefix: This variable is intended to perform directory change operations prior to program execution
75 :param program: The solver program absolute or relative path
76 :param program_required: Space delimited string of required solver program options and arguments that are crucial to
77 builder behavior and should not be modified except by advanced users
78 :param program_options: Space delimited string of optional solver program options and arguments that can be freely
79 modified by the user
80 :param subcommand: The solver program's routine subcommand absolute or relative path
81 :param subcommand_required: Space delimited string of required solver program's routine subcommand options and
82 arguments that are crucial to builder behavior and should not be modified except by advanced users.
83 :param subcommand_options: Space delimited string of optional solver program's routine subcommand options and
84 arguments that can be freely modified by the user
85 :param action_suffix: This variable is intended to perform program STDOUT and STDERR redirection operations.
86 :param emitter: An SCons emitter function. This is not a keyword argument in the action string.
87 :param kwargs: Any additional keyword arguments are passed directly to the SCons builder object.
88
89 :return: Solver journal builder
90 """ # noqa: E501
91 builder = waves.scons_extensions.first_target_builder_factory(
92 environment=environment,
93 action_prefix=action_prefix,
94 program=program,
95 program_required=program_required,
96 program_options=program_options,
97 subcommand=subcommand,
98 subcommand_required=subcommand_required,
99 subcommand_options=subcommand_options,
100 action_suffix=action_suffix,
101 emitter=emitter,
102 **kwargs,
103 )
104 return builder
Example unit tests to verify the builder action string, keywords, emitter, and node count are also provided. These are
similar to the WAVES unit test verification suite for builder facotories based on
waves.scons_extensions.first_target_builder_factory()
and should be the minimum verification implemented for
similar builders.
Unit tests are not sufficient to verify desired solver behavior. System tests that run a real, but minimal, example problem should also be implemented to ensure that the builder action string, task re-execution, file handling expecations, and command line options are appropriate for workflow use. The system tests should be complete enough to verify that the third-party solver behavior still matches the assumptions in the buider design. For WAVES, the tutorials serve as system tests for builder design. For a project specific builder, a minimal working example should be included in the project as an inexpensive verification check and easy-to-use troubleshooting task.
1"""Test the project SCons extensions module."""
2
3import collections
4
5import pytest
6import SCons.Environment
7import scons_extensions
8import waves
9
10
11def dummy_emitter_for_testing(
12 target: list,
13 source: list,
14 env: SCons.Environment.Environment, # noqa: ARG001
15) -> tuple[list, list]:
16 """Return the SCons task's target and source node lists.
17
18 :param target: The target file list of strings
19 :param source: The source file list of SCons.Node.FS.File objects
20 :param env: The builder's SCons construction environment object
21
22 :returns: target, source
23 """
24 return target, source
25
26
27solver_builder_factory_tests = {
28 "default behavior": ({}, {}, ["solver_builder_factory.out0"], False, 2, 1),
29 "different emitter": ({}, {}, ["solver_builder_factory.out1"], dummy_emitter_for_testing, 1, 1),
30 "builder kwargs overrides": (
31 {
32 "environment": "different environment",
33 "action_prefix": "different action prefix",
34 "program": "different program",
35 "program_required": "different program required",
36 "program_options": "different program options",
37 "subcommand": "different subcommand",
38 "subcommand_required": "different subcommand required",
39 "subcommand_options": "different subcommand options",
40 "action_suffix": "different action suffix",
41 },
42 {},
43 ["solver_builder_factory.out2"],
44 False,
45 2,
46 1,
47 ),
48 "task kwargs overrides": (
49 {},
50 {
51 "environment": "different environment",
52 "action_prefix": "different action prefix",
53 "program": "different program",
54 "program_required": "different program required",
55 "program_options": "different program options",
56 "subcommand": "different subcommand",
57 "subcommand_required": "different subcommand required",
58 "subcommand_options": "different subcommand options",
59 "action_suffix": "different action suffix",
60 },
61 ["solver_builder_factory.out3"],
62 False,
63 2,
64 1,
65 ),
66}
67
68
69# TODO: Expose WAVES builder factory test functions for end users
70@pytest.mark.parametrize(
71 ("builder_kwargs", "task_kwargs", "target", "emitter", "expected_node_count", "expected_action_count"),
72 solver_builder_factory_tests.values(),
73 ids=solver_builder_factory_tests.keys(),
74)
75def test_solver_builder_factory(
76 builder_kwargs: dict,
77 task_kwargs: dict,
78 target: list,
79 emitter: collections.abc.Callable[[list, list, SCons.Environment.Environment], tuple[list, list]],
80 expected_node_count: int,
81 expected_action_count: int,
82) -> None:
83 """Test builder factories based on :meth:`waves.scons_extensions.builder_factory`.
84
85 :param builder_kwargs: Keyword arguments unpacked at the builder instantiation
86 :param task_kwargs: Keyword arguments unpacked at the task instantiation
87 :param target: Explicit list of targets provided at the task instantiation
88 :param emitter: A custom factory emitter. Mostly intended as a pass-through check. Set to ``False`` to avoid
89 providing an emitter argument to the builder factory.
90 :param expected_node_count: The expected number of target nodes.
91 :param expected_action_count: The expected number of target node actions.
92 """
93 # Set default expectations to match default argument values
94 expected_kwargs = {
95 "environment": "",
96 "action_prefix": "cd ${TARGET.dir.abspath} &&",
97 "program": "solver.py",
98 "program_required": "",
99 "program_options": "",
100 "subcommand": "implicit",
101 "subcommand_required": "--input-file ${SOURCES[0].abspath} --output-file=${TARGETS[0].abspath} --overwrite",
102 "subcommand_options": "",
103 "action_suffix": "> ${TARGETS[-1].abspath} 2>&1",
104 }
105
106 # Update expected arguments to match test case
107 expected_kwargs.update(builder_kwargs)
108 expected_kwargs.update(task_kwargs)
109 # Expected action matches the pre-SCons-substitution string with newline delimiter
110 expected_action = (
111 "${environment} ${action_prefix} ${program} ${program_required} ${program_options} "
112 "${subcommand} ${subcommand_required} ${subcommand_options} ${action_suffix}"
113 )
114
115 # Handle additional builder kwargs without changing default behavior
116 expected_emitter = waves.scons_extensions.first_target_emitter
117 emitter_handling = {}
118 if emitter is not False:
119 expected_emitter = emitter
120 emitter_handling.update({"emitter": emitter})
121
122 # Test builder object attributes
123 factory = scons_extensions.solver_builder_factory
124 builder = factory(**builder_kwargs, **emitter_handling)
125 assert builder.action.cmd_list == expected_action
126 assert builder.emitter == expected_emitter
127
128 # Assemble the builder and a task to interrogate
129 env = SCons.Environment.Environment()
130 env.Append(BUILDERS={"Builder": builder})
131 nodes = env.Builder(
132 target=target,
133 source=["check_builder_factory.in"],
134 **task_kwargs,
135 )
136
137 # Test task definition node counts, action(s), and task keyword arguments
138 assert len(nodes) == expected_node_count
139 for node in nodes:
140 node.get_executor()
141 assert len(node.executor.action_list) == expected_action_count
142 assert str(node.executor.action_list[0]) == expected_action
143 for node in nodes:
144 for key, expected_value in expected_kwargs.items():
145 assert node.env[key] == expected_value
SConscript#
The task target list depends on the solver options. The builder is not written to manage or inspect the solver command line options and does not include an emitter to match the builder target expectations to the solver command line options. Instead, the end user is expected to manage the target list directly.
As in many of the tutorials, the number of solve cpus is accepted from the project specific command line arguments. The
SConscript file implementing an implicit solver workflow inspects the construction environment solve cpus option prior
to configuring the task to determine the expected target list. Since the solver overrides the output file extension, no
special handling of the --output-file
option is required to truncate or correct the provided extension when the
target list changes.
Since the solver does not provide any overwrite behavior for the log file, a user owned clean options is provided to clean the entire workflow build directory in addition to the known target list. This is necessary to help workflow users purge log files and to avoid solver task failures when the maximum number of log files is reached. More advanced builder authors might choose to implement a log file clean action prior to the solver action. More advanced end users might prefer to use an SCons AddPreAction to perform log file cleaning even when the builder does not. Without either implementation, users must remember to clean their build directory regularly to avoid solver exits from accumulated log files.
1#! /usr/bin/env python
2"""Configure the project's implicit solve workflow."""
3
4import pathlib
5
6Import("env")
7
8build_directory = pathlib.Path(Dir(".").abspath)
9workflow_name = build_directory.name
10
11output_file = "implicit.out"
12if env["solve_cpus"] > 1:
13 target = [f"{output_file}{number}" for number in range(env["solve_cpus"])]
14else:
15 target = [output_file]
16workflow = env.Solver(
17 target=target,
18 source=["implicit.yaml"],
19 solve_cpus=env["solve_cpus"],
20)
21env.Clean(workflow, [Dir(build_directory)])
22env.Alias(workflow_name, workflow)
SConstruct#
The SConstruct file of this tutorial is a stripped down example of the core tutorials. All features of the project
configuration should look familiar to the core tutorials. It implements a variant build directory, solve cpus control,
and environment recovery from the launching shell. Since the solver behaves like a project specific file, the solver
executable is found by absolute path from the tutorial/project directory and the parent directory is added to the
environment PATH
. Besides the default action construction, the project adds a project-specific solver option and the
solve_cpus
task keyword argument.
1#! /usr/bin/env python
2"""Configure the WAVES writing builders tutorial."""
3
4import os
5import pathlib
6
7import scons_extensions
8import waves
9
10AddOption(
11 "--build-dir",
12 dest="variant_dir_base",
13 default="build",
14 nargs=1,
15 type="string",
16 action="store",
17 metavar="DIR",
18 help="SCons build (variant) root directory. Relative or absolute path. (default: '%default')",
19)
20AddOption(
21 "--solve-cpus",
22 dest="solve_cpus",
23 default=1,
24 nargs=1,
25 type="int",
26 action="store",
27 metavar="N",
28 help="Run the solver task using N CPUs. (default: '%default')",
29)
30
31env = waves.scons_extensions.WAVESEnvironment(
32 ENV=os.environ.copy(),
33 variant_dir_base=pathlib.Path(GetOption("variant_dir_base")),
34 solve_cpus=GetOption("solve_cpus"),
35)
36env["ENV"]["PYTHONDONTWRITEBYTECODE"] = 1
37
38# Print failed task *.stdout files
39env.PrintBuildFailures()
40
41# Empty defaults list to avoid building all simulation targets by default
42env.Default()
43
44# Add project builders and scanners
45solver_executable = pathlib.Path("solver.py").resolve()
46env["solver"] = env.AddProgram([solver_executable])
47
48# Users should typically rely on AddProgram behavior for third-party software as above, which checks for execute
49# permissions and PATH resolution. If a tutorial user forgets to change the execute permissions on Linux, or is running
50# on Windows, force execution with the Python interpretter and absolute path.
51#
52# VVV Code specific to making tutorial execution more robust VVV
53if env["solver"] is None:
54 env["solver"] = f"python {solver_executable}"
55# ^^^ Code specific to making tutorial execution more robust ^^^
56
57env.Append(
58 BUILDERS={
59 "Solver": scons_extensions.solver_builder_factory(
60 program=env["solver"], subcommand_options="$(--solve-cpus=${solve_cpus}$)"
61 ),
62 }
63)
64
65# Add simulation targets
66workflow_configurations = [
67 "pytest.scons",
68 "implicit_workflow.scons",
69]
70for workflow in workflow_configurations:
71 build_dir = env["variant_dir_base"] / pathlib.Path(workflow).stem
72 SConscript(workflow, variant_dir=build_dir, exports={"env": env}, duplicate=False)
73
74# Print project local help
75env.ProjectHelp()
Building targets#
First, run the workflow with the default number of solve cpus and observe the output directory.
$ scons implicit_workflow
scons: Reading SConscript files ...
Checking whether /home/roppenheimer/waves-tutorials/tutorial_writing_builders/solver.py program exists.../home/roppenheimer/waves-tutorials/tutorial_writing_builders/solver.py
scons: done reading SConscript files.
scons: Building targets ...
cd /home/roppenheimer/waves-tutorials/tutorial_writing_builders/build/implicit_workflow && solver.py implicit --input-file /home/roppenheimer/waves-tutorials/tutorial_writing_builders/implicit.yaml --output-file=/home/roppenheimer/waves-tutorials/tutorial_writing_builders/build/implicit_workflow/implicit.out --overwrite --solve-cpus=1 > /home/roppenheimer/waves-tutorials/tutorial_writing_builders/build/implicit_workflow/implicit.out.stdout 2>&1
scons: done building targets.
$ find build -type f
build/implicit_workflow/implicit.out
build/implicit_workflow/implicit.out.stdout
build/implicit_workflow/solver.log
Re-run the workflow with two solve cpus. The task re-runs because the target list has changed. With the target list change, the original, single cpu solve output file is left around and not overwritten. A more advanced builder or user task definition is required to clean up previous files when the expected target file list changes. If the solver or builder factory provided an option to merge the multiple cpu output files, it could be possible for the user to write a task that does not re-execute when the number of solve cpus changes. In this tutorial, the user would need to write the merge operation as an SCons AddPostAction to get similar behavior when the requested solver cpus changed.
Observe that the previous log file still exists and a new log file with extension *.log1
is found in the build
output. Cleaning log files to produce deterministic output for task log file tracking and cleaning would require a more
advanced user task definition. It would be tempting to write a builder action that always purges log files prior to
execution; however, if the end user were running multiple solver tasks in this build directory, all log files of
previous tasks would also be removed. In this case, it is probably better to leave log file handling to the end user.
Finally, the user target list construction combined with the builder --output-file
option handling results in the
*.stdout
file name change from implicit.out.stdout
to implicit.out0.stdout
. A builder factory emitter could
be written to look for the solve_cpus
environment varible to match emitted solver output targets to solver output
handling. Changing the *.stdout
naming convention for greater consistency is possible to handle in the builder
factory, but would be more difficult to implement robustly and document clearly, so matching the extension of the first
expected target is probably best.
$ scons implicit_workflow --solve-cpus=2
scons: Reading SConscript files ...
Checking whether /home/roppenheimer/waves-tutorials/tutorial_writing_builders/solver.py program exists.../home/roppenheimer/waves-tutorials/tutorial_writing_builders/solver.py
scons: done reading SConscript files.
scons: Building targets ...
cd /home/roppenheimer/waves-tutorials/tutorial_writing_builders/build/implicit_workflow && solver.py implicit --input-file /home/roppenheimer/waves-tutorials/tutorial_writing_builders/implicit.yaml --output-file=/home/roppenheimer/waves-tutorials/tutorial_writing_builders/build/implicit_workflow/implicit.out0 --overwrite --solve-cpus=2 > /home/roppenheimer/waves-tutorials/tutorial_writing_builders/build/implicit_workflow/implicit.out0.stdout 2>&1
scons: done building targets.
$ find build -type f
build/implicit_workflow/implicit.out0
build/implicit_workflow/implicit.out
build/implicit_workflow/solver.log1
build/implicit_workflow/implicit.out1
build/implicit_workflow/implicit.out.stdout
build/implicit_workflow/implicit.out0.stdout
build/implicit_workflow/solver.log
Run the clean operation with the two solve cpus option and observe that the entire builder directory is removed. This is
necessary for the end user to have consistent log file purging behavior without resorting to shell remove commands.
Removing build directory artifacts by shell command is not necessarily a bad practices, but can build up muscle memory
for commands that are unnecessarily destructive, such as removing the entire build directory with rm -r build
. In a
single workflow project like this tutorial this achieves the same result. But in a project with many or computationally
expensive workflows this muscle memory may result in expensive data loss and scons workflow --clean
operations
should be preferred, but not relied upon, for better data retention habits.
$ scons implicit_workflow --solve-cpus=2 --clean
scons: Reading SConscript files ...
Checking whether /home/roppenheimer/waves-tutorials/tutorial_writing_builders/solver.py program exists.../home/roppenheimer/waves-tutorials/tutorial_writing_builders/solver.py
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed build/implicit_workflow/implicit.out0
Removed build/implicit_workflow/implicit.out1
Removed build/implicit_workflow/implicit.out0.stdout
Removed build/implicit_workflow/implicit.out
Removed build/implicit_workflow/implicit.out.stdout
Removed build/implicit_workflow/solver.log
Removed build/implicit_workflow/solver.log1
Removed directory build/implicit_workflow
scons: done cleaning targets.
$ find build -type f
$