Examples
Source files are at https://github.com/pnnl/pecblocks/tree/master/examples/hwpv. Date files are at the Harvard Dataverse. Results from these examples are discussed in Draft Paper
To create their own examples, users will need to provide:
A configuration file, as described in Schema
A training dataset from laboratory tests or EMT simulations, as described in Data from EMT
Training a Model
The following script trains a model on Windows. It’s called with one argument, the root of the configuration file, e.g., call train.bat osg4, assuming that osg4_config.json has been created. The directory osg4 will be created if necessary, but not erased. The last line plots training losses to a saved PDF, but not to the screen so that a calling script may continue without user acknowledgement.
if not exist %1\ mkdir %1
python pv3_training.py %1_config.json
python loss_plot.py %1 -1
The followng script is for Linux and Mac OS X, called like ./train.sh osg4
#!/bin/bash
if [ ! -d "$1" ]; then
echo "creating directory $1"
mkdir $1
fi
python3 pv3_training.py $1_config.json
python3 loss_plot.py $1 -1
The following Python code supervises training a model using pecblocks.
Line 12 disables the output of mean absolute error (MAE), because root mean square error (RMSE) is always more suitable for HWPV applications.
Line 13 disables the plot of losses at the end of a training run. This plot waits for user input, which prevents a batch file from completing several training runs. To plot the losses afterward, invoke python loss_plot.py osg4
Lines 15-30 open the required configuration file.
Lines 32-38 comprise the main usage of pecblocks API
Line 32 initializes a model instance from the JSON configuration file
Line 33 loads the training data set, which may take several seconds or longer
Line 34 normalizes the training data to a range 0..1 on each channel, and saves the normalization factors
Line 35 prepares the model for training (and evaluation)
Line 36 trains new model coefficients, which may take several minutes or longer. Progress updates are written to the console.
Line 37 saves the most recent trained model coefficients. (Note: these coefficients are saved periodically during training. The set of coefficients with lowest fitting loss is also saved as needed in each epoch.)
Line 38 summarizes the training errors for each output channel, over all cases or events in the training data set. More detail is available in the next steps. The RMSE is most relevant to HWPV applications.
Lines 40-59 provide summary output of the training run. This information is also availble later.
Lines 61-73 plot the losses vs. training epoch. Use of loss_plot.py is now preferred.
1# copyright 2021-2024 Battelle Memorial Institute
2# supervises training of HW model for a three-phase inverter
3# arg1: relative path to the model configuration file
4
5import numpy as np
6import os
7import sys
8import json
9import matplotlib.pyplot as plt
10import pecblocks.pv3_poly as pv3_model
11
12bWantMAE = False
13bWantPlot = False
14
15if __name__ == '__main__':
16 if len(sys.argv) > 1:
17 config_file = sys.argv[1]
18 fp = open (config_file, 'r')
19 cfg = json.load (fp)
20 fp.close()
21 data_path = cfg['data_path']
22 model_folder = cfg['model_folder']
23 model_root = cfg['model_root']
24 else:
25 print ('Usage: python pv3_training.py config.json')
26 quit()
27
28 print ('model_folder =', model_folder)
29 print ('model_root =', model_root)
30 print ('data_path =', data_path)
31
32 model = pv3_model.pv3(training_config=config_file)
33 model.loadTrainingData(data_path)
34 model.applyAndSaveNormalization()
35 model.initializeModelStructure()
36 train_time, LOSS, VALID, SENS = model.trainModelCoefficients(bMAE=False)
37 model.saveModelCoefficients()
38 rmse, mae, case_rmse, case_mae = model.trainingErrors(bByCase=False)
39
40 nlookback = 10
41 recent_loss = LOSS[len(LOSS)-nlookback:]
42
43 fp = open (os.path.join (model_folder, 'summary.txt'), 'w')
44 jp = open (os.path.join (model_folder, 'normfacs.json'), 'r')
45 normfacs = json.load (jp)
46 jp.close()
47 print ('Dataset Summary: (Mean=Offset, Range=Scale)', file=fp)
48 print ('Column Min Max Mean Range', file=fp)
49 for key, row in normfacs.items():
50 print ('{:6s} {:9.3f} {:9.3f} {:9.3f} {:9.3f}'.format (key, row['min'], row['max'], row['offset'], row['scale']), file=fp)
51
52 out_size = len(model.COL_Y)
53 recent_loss = np.mean(recent_loss)
54 valstr = ' '.join('{:.4f}'.format(rmse[j]) for j in range(out_size))
55 print ('COL_Y', model.COL_Y, file=fp)
56 print ('Train time: {:.2f}, Recent loss: {:.6f}, RMS Errors: {:s}'.format (train_time, recent_loss, valstr), file=fp)
57 print ('COL_Y', model.COL_Y)
58 print ('Train time: {:.2f}, Recent loss: {:.6f}, RMS Errors: {:s}'.format (train_time, recent_loss, valstr))
59 if bWantMAE:
60 valstr = ' '.join('{:.4f}'.format(mae[j]) for j in range(out_size))
61 print (' MAE Errors: {:s}'.format (valstr), file=fp)
62 fp.close()
63
64 if bWantPlot:
65 plt.figure()
66 plt.title(model_root)
67 plt.plot(np.log10(LOSS), label='Training Loss')
68 plt.plot(np.log10(VALID), label='Validation Loss')
69 if np.min(SENS) > 0.0:
70 plt.plot(np.log10(SENS), label='Sensitivity Loss')
71 plt.ylabel ('Log10')
72 plt.xlabel ('Epoch')
73 plt.legend()
74 plt.grid(True)
75 plt.savefig(os.path.join(model_folder, '{:s}_train_loss.pdf'.format(model_root)))
76 plt.show()
77
Exporting a Model
The following script exports a trained model for simulation, and its accuracy metrics on Windows. It’s called with one argument, the root of the configuration file, e.g., call export.bat osg4, assuming that osg4_config.json has been created. Furthermore, the directory osg4 should have been created and populated by training the model.
python pv3_export.py %1_config.json > %1\metrics.txt
python pv3_metrics.py %1_config.json >> %1\metrics.txt
The followng script is for Linux and Mac OS X, called like ./export.sh osg4
#!/bin/bash
python3 pv3_export.py $1_config.json > $1/metrics.txt
python3 pv3_metrics.py $1_config.json >> $1/metrics.txt
The following Python code exports (post-processes) a trained model for simulation using pecblocks.
Lines 10-27 open the required configuration file
Lines 29-34 comprise the main usage of pecblocks API
Line 29 initializes a model instance from the JSON configuration file, same as for training.
Line 30 loads the normalization factors from training. To export the model, it’s not necessary to load the training data set.
Line 31 prepares the model for export
Line 32 loads the trained model coefficients from binary pkl files, which are not human-readable.
Line 33 exports the trained model coefficients in human-readable JSON file. During this process, and s-domain version of the model is prepared for simulation at variable time step. The JSON file is readable by other applications in a variety of languages, including Python, C/C++, and MATLAB.
Line 34 checks for stability of the exported H1 poles. If there are any warnings about unstable poles, do not use this model for simulation. Specifying the stable2ndx type for H1 in a re-trained model should avoid this issue.
1# copyright 2021-2024 Battelle Memorial Institute
2# exports a trained HW model and normalization coefficients to a single JSON file
3# arg1: relative path to the model configuration file
4
5import os
6import sys
7import json
8import pecblocks.pv3_poly as pv3_model
9
10if __name__ == '__main__':
11 if len(sys.argv) > 1:
12 config_file = sys.argv[1]
13 fp = open (config_file, 'r')
14 cfg = json.load (fp)
15 fp.close()
16 model_folder = cfg['model_folder']
17 model_root = cfg['model_root']
18 data_path = cfg['data_path']
19 else:
20 print ('Usage: python pv3_export.py config.json')
21 quit()
22 export_path = os.path.join(model_folder,'{:s}_fhf.json'.format(model_root))
23 if len(sys.argv) > 2:
24 export_path = sys.argv[2]
25
26 print ('Read Model from:', model_folder)
27 print ('Export Model to:', export_path)
28
29 model = pv3_model.pv3(training_config=config_file)
30 model.loadNormalization()
31 model.initializeModelStructure()
32 model.loadModelCoefficients()
33 model.exportModel(export_path)
34 model.check_poles()
35
Model Metrics
The following Python code summarizes metrics of a trained model using pecblocks.
Line 11 disables the output of mean absolute error (MAE), because root mean square error (RMSE) is always more suitable for HWPV applications.
Lines 13-28 open the required configuration file.
Lines 30-35 comprise the main usage of pecblocks API
Line 30 initializes a model instance from the JSON configuration file, same as for training or exporting.
Line 31 loads the training data set, which may take several seconds or longer
Line 32 loads and applies normalization factors established during training.
Line 33 prepares the model for evaluation (and training)
Line 34 loads the trained model coefficients from binary pkl files, which are not human-readable.
Line 35 summarizes the training errors for each output channel, individually by case or event, and over all cases.
Lines 36-76 provide summary output, which includes:
The RMSE for each output channel and each case.
Identifying the case number that produced the highest RMSE for each output channel.
The number of cases in which the RMSE exceeded 0.05 per-unit, by output channel. This should be considered in judging whether the trained model is acceptable for use.
The total RMSE over all cases, for each output channel. These values are the same as output at the end of the training run. However, some cases will have higher RMSE values. Furthermore, some cases may exceed 0.05 RMSE, even when the total RMSE does not exceed 0.05 perunit.
1# copyright 2021-2024 Battelle Memorial Institute
2# summarizes RMSE and MAE for all training cases of an HW model for three-phase inverters
3# arg1: relative path to the model configuration file
4
5import os
6import sys
7import json
8
9import pecblocks.pv3_poly as pv3_model
10
11bWantMAE = False
12
13if __name__ == '__main__':
14 if len(sys.argv) > 1:
15 config_file = sys.argv[1]
16 fp = open (config_file, 'r')
17 cfg = json.load (fp)
18 fp.close()
19 data_path = cfg['data_path']
20 model_folder = cfg['model_folder']
21 model_root = cfg['model_root']
22 else:
23 print ('Usage: python pv3_metrics.py config.json')
24 quit()
25
26 print ('model_folder =', model_folder)
27 print ('model_root =', model_root)
28 print ('data_path =', data_path)
29
30 model = pv3_model.pv3(training_config=config_file)
31 model.loadTrainingData(data_path)
32 model.loadAndApplyNormalization()
33 model.initializeModelStructure()
34 model.loadModelCoefficients()
35 rmse, mae, case_rmse, case_mae = model.trainingErrors(True)
36 out_size = len(model.COL_Y)
37 colstr = ','.join('{:s}'.format(col) for col in model.COL_Y)
38 h1str = ','.join('{:s}'.format('RMSE') for col in model.COL_Y)
39 valstr = ','.join('{:.4f}'.format(rmse[j]) for j in range(out_size))
40 if bWantMAE:
41 h2str = ','.join('{:s}'.format('MAE') for col in model.COL_Y)
42 maestr = ','.join('{:.4f}'.format(mae[j]) for j in range(out_size))
43 print ('Idx,{:s},{:s}'.format(colstr, colstr))
44 print ('#,{:s},{:s}'.format(h1str, h2str))
45 print ('Total,{:s},{:s}'.format(valstr, maestr))
46 for i in range(len(case_rmse)):
47 valstr = ','.join('{:.4f}'.format(case_rmse[i][j]) for j in range(out_size))
48 maestr = ','.join('{:.4f}'.format(case_mae[i][j]) for j in range(out_size))
49 print ('{:d},{:s},{:s}'.format(i, valstr, maestr))
50 print ('Total Error Summary')
51 for j in range(out_size):
52 print ('{:4s} MAE={:8.4f} RMSE={:8.4f}'.format (model.COL_Y[j], mae[j], rmse[j]))
53 else:
54 print ('Idx,{:s}'.format(colstr))
55 print ('#,{:s}'.format(h1str))
56 print ('Total,{:s}'.format(valstr))
57 for i in range(len(case_rmse)):
58 valstr = ','.join('{:.4f}'.format(case_rmse[i][j]) for j in range(out_size))
59 print ('{:d},{:s}'.format(i, valstr))
60 print ('Highest RMSE Cases')
61 for j in range(out_size):
62 mval = 0.0
63 idx = 0
64 nmax = 0
65 for i in range(len(case_rmse)):
66 val = case_rmse[i][j]
67 if val > 0.05:
68 nmax += 1
69 if val > mval:
70 idx = i
71 mval = val
72 print ('{:4s} Max RMSE={:8.4f} at Case {:d}; {:d} > 0.05'.format (model.COL_Y[j],
73 mval, idx, nmax))
74 print ('Total Error Summary')
75 for j in range(out_size):
76 print ('{:4s} RMSE={:8.4f}'.format (model.COL_Y[j], rmse[j]))
Running a Model
Once the model has been exported, it can be run in a dynamic or EMT simulation with forward evaluation of the HWPV blocks. Two examples are described below. In addition, the models have been run in MATLAB/Simscape and ATP, with a Cigre/IEEE “real code” DLL implementation in progress.
Co-simulation using HELICS
This example runs in the z domain, with a fixed time step that must match the time step used to train the model. It runs as a co-simulation, with two Python federates communicating over the HELICS interface. One federate plays the role of the grid, while the other plays the role of a HWPV inverter model. In a more realistic use case, the grid federate might be a commercial dynamics or EMT simulator that already supports HELICS.
See IEEE Access Paper for more information about HELICS.
The example files and a description are at https://github.com/pnnl/pecblocks/tree/master/examples/helics
Standalone Simulation using Python
This example runs in the s domain using a Backward Euler integration of the H1 block. The time step is longer than the step used to train the model. Although not shown in this example, the Backward Euler method allows for variable time step during a simulation.
The example files and a description are at https://github.com/pnnl/pecblocks/tree/master/examples/pi
The file hwpv_pi.py plays the role of a grid federate, applying the test stimuli, calculating voltage from current at the PCC, and calling the HWPV model at each time step
The file hwpv_evaluator.py implements the HWPV block model using numpy, but not pecblocks, dynonet, or torch. It can run on a Raspberry Pi or larger computer.
The evaluator supports the z domain, the s domain with Forward Euler (less accurate and possibly unstable), and the s domain with Backward Euler (preferred).