CONTRIBUTING.md 19.2 KB
Newer Older
1
2
# Contributing

3
This documentation provides guidelines for new PyARC developers to learn about development practices. Thank you for your contributions!
4
5
6

## Table of Contents

7
1. [Developer Code of Conduct](#code) - Best practices recommendations for new developers
8
9
10
2. [Version Control Workflow](#version-control-workflow) - Adding new codes or features to PyARC
3. [Unit Tests](#unit-tests) - Unit-tests implementation
4. [Integration Steps](#new-code-integration-steps) - Description of steps for new code integration
11
    * [Input Structure](#input-structure) - Definition of input structure
12
    * [New Code Addition](#new-code-addition) - Workflow definition
13
    * [Tutorial](#tutorial) - Tutorial development
Patrick Shriwise's avatar
Patrick Shriwise committed
14
5. [Code Integration Example](#code-integration-example) - Example of code integration
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
15
    * [Schema](#schema) - Developing in the Schema
Patrick Shriwise's avatar
Patrick Shriwise committed
16
    * [PyMyCode Example](#pymycode) - Structure of PyCode
17
18
19
20
        * [Pre Check](#precheck) - Early check logic
        * [Pre Run](#prerun) - Pre-run logic
        * [Execution](#execution) - Execution logic
        * [Post Run](#postrun) - Post-run logic
21
    * [Integration in PyARC](#pyarc-integration) - Integration of PyCode into PyARC
22
    * [Unit Test](#unit-test) - Example of unit-test
23
6. [Additional resources](#resources) - More Gitlab resources
24
25
26

# Developer Code of Conduct

27
28
The code of conduct for developer interactions can be found
[here](./CODE_OF_CONDUCT.md). Please report any unacceptable behavior to
29
pyarc@anl.gov.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
30

31
# Version control workflow
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
32

33
34
35
36
37
38
39
40
41
42
Please follow the traditional collaborative development guidelines:
1. Work from Gitlab, in a branch off of `development`, and frequently push your
   changes to the repository to simplify review. Create issues and reference
   them when appropriate.
2. Once you have completed your development, complete a Merge Request (from your
   branch to `development`) and request review of your code from another
   developer. Merged changes will be included in new versions of PyARC by
   merging the `development` branch into the `master` branch.
3. Strive for high quality code rather than quick demonstration. Improving code
   afterward is always more time consuming than improving it upfront.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
43

44
If you do not have access to push branches and/or merge the PyARC repository,
45
please contact either pyarc@anl.gov to request access.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
46

47
# Unit-tests
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
48

49
50
51
52
Unit tests are automatically run everytime changes are pushed to the repository.
They are also a very important tool for developers who can manually run them
during the development work to verify his code development did not affect other
parts of PyARC.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
53
54
55
56
57
58

Execution of unit tests:
* all unit tests: `Workbench-path/rte/entry.sh -m unittest discover -v`
* specific unit test: `Workbench-path/rte/entry.sh -m unittest discover -p test_pyarc_mytest.py`

Unit tests must be setup to test:
59
60
* pre-check in [test_pyarc_precheck](/test/test_pyarc_precheck.py): a few unit-tests may be required to check the `precheck` logic and verify the error messages returned.
* pre-run in [test_pyarc_prerun](/test/test_pyarc_prerun.py): typically 5-10 unit tests may be needed to test out verious options of the code. In this process, the generated input by PyARC is compared line-by-line with a reference one located in `test/data/`.
61
* post-run in [test_pyarc_postrun](/test/test_pyarc_postrun.py): typically, 1-2 unit tests may be needed to test out post-processing logic. In this process, the generated summary files will be compared line-by-line with a reference one located in `test/data/`.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
62
63
* execution in [test_pyarc_run](/test/test_pyarc_run.py): 1 unit test is typically used to test out the execution. In this process, a few results from the calculation will be compared with reference values.
* tutorial in [test_pyarc_run_tutorial](/test/test_pyarc_run_tutorial.py): it is important to automatically verify the proper execution of the tutorial results to verify it is up-to-date.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
64

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# New Code Integration Steps

Please review the following integration steps. They should generally be
completed in the following order. Each and every one of these steps needs to be
completed whether you are integrating a new code to PyARC or adding an option
within a code (some of the steps may be straightforward in this case).

## Input Structure

For development requiring changes in input definition (new input option), the
first step is to define the input structure specifying for each proposed option:
* Required/optional, dependencies with other options?
* Range of appearance
* Types of value, ranges of their values or list of acceptable values
* etc.

This input definition is then used to update the [schema](etc/arc.sch) and
develop templates (provided to the used during autocompletion), as discussed in
[Schema](#schema). **It is important to thoroughly test your input from the
Workbench before moving to the next step.**

## New Code Addition

The workflow definition of new code integration is implemented within a class
that contains the following methods:

* pre-check: list of initial checks that can be done on the input. Such additional tests may be required due to limitation in Workbench input interpretor
* pre-run: define the pre-processing logic, extracting the information from the PyARC input and translate it into the native code input.
* execution: describe the execution logic. It is usually very straightforward (build temporary directory and execute the input there before cleaning up).
* post-run: define the post-processing logic, extracting the information from the output and translate it into the suggested PyARC `*.summary` and `*.extended.summary` files.

If pre-processing or execution failures are observed, any remaining steps in the
code should be skippped and an error message should be provided to the user.

The Workflow can be made more complex by calling sub-codes (like MCC3 is being
called when running PERSENT). In this case, prerun of MCC3 is called from prerun
of PERSENT.

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
103
104
## Tutorial

105
106
107
108
109
Here is the link to existing [Tutorials](/tutorial/README.md#tutorial). Code
integration is completed only when tutorial is developed with proper
documentation. User input files (based on ABTR core design if possible, to be
consistent with other tutorial) needs to be accompanied by expected summary
files, and by a `README.md` file detailing the input and output.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
110

111
# Code Integration Example
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
112

113
114
In this example we'll integrate a code named `MyCode` into PyARC. We will define
`PyMyCode.py` file and describe its key components.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
115
116
117

## Schema

118
119
The following input example needs to be develop to understand the different
options that should be defined in the PyARC schema.
120
121
122
123
124

```javascript
mycode ( block_id ) {  # optional, can appear several time - no limit. Block ID is required, and take any string
        my_value = 5 # required, integer between 1 to 10 (included), default = 1
        my_list = [ val1 val2 val3 val4 val5 ] # required, takes string, number of string specified by my_value, each is unique
125
    }
126
127
```

128
129
130
131
132
133
This input definition can be translated into [__schema__](etc/arc.sch) as
following. More information about the SON input structure is available in
[WASPON](https://code.ornl.gov/neams-workbench/wasp/-/blob/master/waspson/README.md#standard-object-notation-son)
documentation, and more information on schema language structure is available in
[HIVE](https://code.ornl.gov/neams-workbench/wasp/-/blob/master/wasphive/README.md#hive)
documentation.
134

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
135
```javascript
136
mycode{
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
137
138
139
    Description = "[optional] my block description"
    MinOccurs=0
    MaxOccurs=NoLimit
140
    InputTmpl="mycode"
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
141
142
143
144
145
146
147
148
149
150
151
152
153
    id{
        MinOccurs=1
        MaxOccurs=1
        ValType=String
    }
    my_value{
        Description = "[required] description of my_value"
        MinOccurs=1
        MaxOccurs=1
        ValType=Int
        MinValInc=1
        MaxValInc=10
        InputTmpl="flagtypes"
154
        InputDefault=1
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
155
156
157
158
159
160
    }
    my_list{
        Description = "[required] description of my_list"
        InputTmpl="sonarray"
        MinOccurs=1
        MaxOccurs=1
161
        ChildUniqueness  = [ value ]
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
162
163
164
165
166
167
168
169
170
        value{
            MaxOccurs="../../my_value"
            MaxOccurs="../../my_value"
            ValType=String
        }
    }
}
```

171
172
173
This should be associated with a `mycode.tmpl` located in `etc/templates` that
would look like bellow. The template will be provided back to the user when they
use the autocompletion function within the Workbench.
174

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
175
```javascript
176
177
178
mycode ( block_id ) {
    my_value = 1
    my_list = [ list_val ]
179
}
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
180
```
181

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
182
183
## PyMyCode

184
185
186
187
188
Below is a summary of the structure of [PyMyCode](PyMyCode.py) that is being
developed. It takes 5 blocks. First, the `__init__` block defines variables
within the MyCode class. Then, the `workflow` block provides the workflow,
calling for `prerun`, `run`, and `postrun` procedures. The `precheck` procedure
is called much earlier, during the early steps of `PyARC`.
189

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
```javascript
class MyCode(object):
    """Defines mycode integration procedures"""
    def __init__(self, user_object, do_prerun = False, do_run = False, do_postrun = False):
        self.user_object = user_object
        self.do_prerun = do_prerun
        self.do_run = do_run
        self.do_postrun = do_postrun
        self.list_mycode_inp_path = []
        self.list_mycode_out_path = []

    def workflow(self):
        """Defines the workflow for mycode calculations"""
        self.user_object.print_timelines("mycode calculation")
        mycode = self.user_object.calculations_input().mycode
        for step_calc in range(len(mycode.depletion_steps.value)):
            num_time_step = int(str(mycode.depletion_steps.value[step_calc]))
            time_step = self.user_object.list_time_steps[num_time_step]
            if self.do_prerun:
                mycode_crash = self.prerun(time_step)
210
                if mycode_crash:
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
211
212
213
214
215
216
217
218
                    self.do_run = False
                    self.do_postrun = False
            if self.do_run:
                mycode_crash = self.run(time_step)
                if mycode_crash: self.do_postrun = False
            if self.do_postrun:
                self.postrun(time_step)

219
220
221
222
223
    def precheck(self):
        """Perform various early checks on the input and associated files - crash before any pre-processing on any codes if mycode_crash is True"""
        mycode_crash = False
        return mycode_crash

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
224
225
226
227
228
229
230
231
232
233
234
235
    def prerun(self, time_step):
        """Write mycode input file"""
        return mycode_crash

    def run(self, timestep):
        """ Procedure that executes mycode inputs and returns the output """
        return mycode_crash

    def postrun(self, timestep):
        """Extracts results from mycode output file"""
```

236
237
### Precheck

238
239
240
241
242
243
Below is an extended example of the `precheck` block. The main role of this
block are to detect issues and crash the code with error message before PyARC
start processing any input (from any codes). The objective if to complete
additional checks on the user input that the Workbench input logic cannot
handle. Example of checks are listed below:

244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
* Check executable file is available
* Perform complex geometry checks (outer radius is larger than inner radius, etc.)
* etc.

```javascript
    def precheck(self):
        """Perform various early checks on the input and associated files - crash before any pre-processing on any codes if mycode_crash is True"""
        mycode_crash = False

        # check 1 - executable is available
        mycode_exec = "%s/mycode.x"%self.user_object.arc_executable
        if os.path.isfile(mycode_exec) is False:
            mycode_crash = False # should be True, but kept to false since the executable is not meant to exist!
            self.user_object.write_err_lines("ERROR: Executable %s does not exist"%mycode_exec)

        return mycode_crash
```
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
261
262
263

### Prerun

264
265
266
267
268
Below is an extended example of the `prerun` block. The main role of this block are:
* create `mycode.inp` input file
* extract the information from the PyARC `.son` input file, process it (if needed)
* write the `mycode.inp` input file
* optionally: detect issues and crash the code with error message
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
269
270

```javascript
271
272
    def prerun(self):
        """Write MyCode input file"""
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
273
274
275
        calculations = self.user_object.calculations_input()
        mycode = calculations.mycode
        mycode_crash = False
276
277
278
279
280
281
282
283
284
285
286
287
288
289
        self.user_object.print_timelines("   Pre-run  MyCode")
        mycode_input_file  = open(self.mycode_inp_path, "w")

        # TODO: write mycode input logic - here is an example highlighting how to access the data from the PyARC input
        mycode_input_file.writelines("mycode - V0.0.0")
        if calculations.mycode is not None:
            for it, mycode_block in enumerate(calculations.mycode):
                mycode_id = str(mycode_block.id).lower()
                mycode_input_file.writelines("\n  Block: %s"%mycode_id)
                num_val = int(str(mycode_block.my_value.value).lower())
                mycode_input_file.writelines("\n  Block Values: %i"%num_val)
                for val in range(num_val):
                    item = str(mycode_block.my_list.value[val])
                    mycode_input_file.writelines("\n    Block Item %i: %s"%(val, item))
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
290
291

        mycode_input_file.close()
292
        append_lines(self.mycode_inp_path, self.user_object.main_input_file_name)
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
293
294
295
296
297
        return mycode_crash
```

### Execution

298
299
300
301
302
303
Below is an extended example of the `run` block. The main role of this block are:
* create the temporary directory `tmp_mycode` to run the code
* execute the code
* save the `mycode.out` output file
* detect code crash and return error message
* cleanup the directory
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
304
305

```javascript
306
307
    def run(self):
        """ Procedure that executes MyCode inputs and returns the output """
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
308
        mycode_crash = False
309
310
311
        self.user_object.print_timelines("   Run  MyCode")
        input_name = self.mycode_inp_path

Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
312
313
314
315
316
        mycode_path = "%s/mycode.x"%self.user_object.arc_executable
        if os.path.exists("tmp_mycode") == False:
            os.mkdir("tmp_mycode")
        os.chdir("tmp_mycode")

317
        os.system("%s < %s > %s"%(mycode_path, self.mycode_inp_path, self.mycode_out_path)) # example of execution logic
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
318

319
320
        mycode_output_file = open(self.mycode_out_path, "w")
        mycode_output_file.writelines("MyCode output")
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
321
        mycode_output_file.close()
322
        shutil.copy(self.mycode_out_path, "../")
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
323

324
        append(self.mycode_out_path, self.user_object.output_path)
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
325

326
        lines1 = open(self.mycode_out_path, "r").readlines()
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
327
        for l in range(len(lines1)):
328
329
            if "FATAL ERROR MESSAGE FROM MyCode" in lines1[l]:
                self.user_object.write_err_lines("ERROR: MyCode simulation has crashed")
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
330
331
332
333
334
335
336
337
338
339
340
341
342
343
                error_msg = ""
                error_lines = min(len(lines1), 20)
                for j in range(error_lines):
                    error_msg += "\n"+lines1[l+(j-error_lines+2)]
                self.user_object.write_err_lines(error_msg)
                mycode_crash = True

        os.chdir("..")
        shutil.rmtree("tmp_mycode")
        return mycode_crash
```

### Postrun

344
345
346
347
Below is an extended example of the `postrun` block. The main role of this block are:
* read the `mycode.out` output file and extract information
* create and populate the `mycode.summary` file with the main information that is the most useful to the user (results per assembly or areas)
* create and populate the `mycode.extended.summary` filed with more information (results for each region)
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
348
349

```javascript
350
351
352
    def postrun(self):
        """Extracts results from MyCode output file"""
        self.user_object.print_timelines("   Post-run MyCode")
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
353

354
        output = self.mycode_out_path + ".summary"
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
355
        out = open(output, "w")
356
        out.writelines("\n\n************************ MyCode SUMMARY ***************************\n")
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
357

358
        output_ex = self.mycode_out_path + ".extended.summary"
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
359
        outex = open(output_ex, "w")
360
        outex.writelines("\n\n************************ MyCode SUMMARY  ***************************\n")
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
361
362


363
        mycode_output = open(self.mycode_out_path, "r").readlines()
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
364
365

        # TODO: implement post-processing logic
366

367
        self.user_object.results['mycode'] = 0 # provide some results that we think users will always be interested. This will be used for unit-tests (check results do not change) and can be extracted from Python execution
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
368
369
370
371
372
373
374
375
376

        out.writelines("\n")
        out.close()
        append(output, self.user_object.output_summary_path)

        outex.writelines("\n")
        outex.close()
        append(output_ex, self.user_object.output_extended_summary_path)
```
377
378
379

## PyARC Integration

380
In [PyARC](PyARC.py) and [PyARCUserObject](PyARCUserObject.py), check where `mycode` is being called for to understand how to bring it in the overall PyARC workflow. The following lines in particular provide where in the PyARC workflow is MyCode being executed (at the end, in our case).
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
381

382
383
384
385
386
387
```javascript
if self.user_object.invoke_mycode():
    self.mycode.workflow()
```

## Unit Test
388

389
See the example below that is in [test_pyarc](test/test_pyarc_input_generation.py). The input example in [arcbench_test0_mycode.son](test/data/arcbench_test0_mycode.son) is being executed only to est the **prerun** logic. The automatically generated `mycode.inp` input is compared line-by-line with the reference [test0_arcbench_test0_mycode.inp](test/data/test0_arcbench_test0_mycode.inp) input that was stored by the developer.
Stauff, Nicolas Emile's avatar
Stauff, Nicolas Emile committed
390

391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
```javascript
    def test_PyARC_input0_mycode(self):
        """ Fake unit test to monstrate unit-tests logic applied to MyCode Fake integrated code """
        this = PyARC.PyARC()
        input_file = self.current_directory+"/data/arcbench_test0_mycode.son"
        options = PyARC.RunOptions()
        options.input = input_file
        od = self.current_directory
        wd = self.current_directory+"/arcbench_test0_mycode"
        this = PyARC.PyARC()
        this.user_object.do_run = False
        this.user_object.do_postrun = False
        this.execute(["-i",input_file,"-w",wd,"-o",od])

        generated_input = this.working_dirs[input_file]+"/mycode.inp"
        reference_input = self.current_directory+"/data/test0_arcbench_test0_mycode.inp"
        self.assertTrue(self.diff_file(generated_input, reference_input))

        shutil.rmtree(wd)
        os.remove(input_file.replace(".son",".isotxs_R_0"))
        os.remove(input_file.replace(".son",".inp"))
        os.remove(input_file.replace(".son",".zip"))
        os.remove(input_file.replace(".son",".timeline.out"))
        os.remove(input_file.replace(".son",".summary"))
        os.remove(input_file.replace(".son",".extended.summary"))
        os.remove(input_file.replace(".son",".user_info.out"))
```
418

Patrick Shriwise's avatar
Patrick Shriwise committed
419
# Additional Resources
420

421
Useful git commands:
422

423
424
425
426
427
428
429
```shell
    git pull # pull new changes from the PyARC's gitlab repository into the current branch.
    git status # check on the status of changed files in the local repository.
    git add <filename> # Add file "filename" to staged changes of the repository.
    git commit –m “my commit message” # Commit staged changes with the specified message.
    git push origin <branch_name> # Push a branch to the gitlab repository.
    git rm <filename> # Stage the removal of "filename" for the next commit.
430
```
431
432
433
434
435
436
437

For additional git commands and explanations, please see this git
[cheatsheet](https://training.github.com/downloads/github-git-cheat-sheet.pdf).

For information on effective communication on Gitlab (opening issues, merge
requests, performing code reviews, etc.), here is a link to
[Gitlab training](https://www.linkedin.com/learning/learning-gitlab-2/version-control-and-more?u=74472898).