├── LICENSE ├── PINN.py ├── README.md ├── create_data.py ├── dataset_handling.py ├── environment.yml ├── power_system_functions.py ├── setup_and_run.py └── train_model.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jochen Stiasny, Georgios Misyris, Spyros Chatzivasileiadis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PINN.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | from power_system_functions import create_system_matrices 3 | import numpy as np 4 | 5 | 6 | class PinnModel(tf.keras.models.Model): 7 | """ 8 | The PINN that incorporates the simple DenseCoreNetwork and adds the physics. 9 | """ 10 | def __init__(self, neurons_in_hidden_layer, power_system, case='normal', 11 | seed=12345, *args, 12 | **kwargs): 13 | super().__init__(*args, **kwargs) 14 | tf.random.set_seed(seed) 15 | 16 | self.DenseCoreNetwork = DenseCoreNetwork(n_states=power_system['n_states'], 17 | neurons_in_hidden_layer=neurons_in_hidden_layer, 18 | output_offset=power_system['output_offset']) 19 | self.n_states = power_system['n_states'] 20 | self.n_generators = power_system['n_generators'] 21 | self.n_buses = power_system['n_buses'] 22 | self.loss_terms = 3 * self.n_states 23 | self.bus_disturbance = 4 24 | self.epoch_count = 0 25 | self.seed = seed 26 | 27 | self.A, self.B, self.C, self.D, self.F, self.G, self.u_0, self.x_0, = create_system_matrices( 28 | power_system=power_system, case=case) 29 | 30 | self.build(input_shape=[(None, 1), (None, self.n_buses)]) 31 | 32 | def call(self, inputs, training=None, mask=None): 33 | x_time, x_power = inputs 34 | x_power_selective = x_power[:, self.bus_disturbance:self.bus_disturbance+1] 35 | 36 | network_output, network_output_t = self.calculate_time_derivatives(inputs=[x_time, x_power_selective]) 37 | 38 | FCX = tf.sin(network_output @ self.C.T) @ self.D.T 39 | 40 | u_disturbance = x_power @ self.G.T 41 | u = u_disturbance + self.u_0.T 42 | 43 | network_output_physics = network_output_t - (network_output @ self.A.T + FCX @ self.F.T + u @ self.B.T) 44 | 45 | return_variables_splits = tf.split(network_output, num_or_size_splits=self.n_states, axis=1) + tf.split( 46 | network_output_t, num_or_size_splits=self.n_states, axis=1) + tf.split(network_output_physics, 47 | num_or_size_splits=self.n_states, 48 | axis=1) 49 | 50 | return return_variables_splits 51 | 52 | def predict_states(self, inputs): 53 | 54 | x_time_np, x_power_np = inputs 55 | 56 | x_time = tf.convert_to_tensor(x_time_np, dtype=tf.float32) 57 | x_power = tf.convert_to_tensor(x_power_np, dtype=tf.float32) 58 | x_power_selective = x_power[:, self.bus_disturbance:self.bus_disturbance+1] 59 | 60 | network_output, network_output_t = self.calculate_time_derivatives(inputs=[x_time, x_power_selective]) 61 | 62 | FCX = tf.sin(network_output @ self.C.T) @ self.D.T 63 | 64 | u_disturbance = x_power @ self.G.T 65 | u = u_disturbance + self.u_0.T 66 | 67 | network_output_physics = network_output_t - (network_output @ self.A.T + FCX @ self.F.T + u @ self.B.T) 68 | 69 | return network_output.numpy(), network_output_t.numpy(), network_output_physics.numpy() 70 | 71 | def calculate_time_derivatives(self, inputs, **kwargs): 72 | time_input, _ = inputs 73 | 74 | list_network_output = [] 75 | list_network_output_t = [] 76 | 77 | for state in range(self.n_states): 78 | with tf.GradientTape(watch_accessed_variables=False, 79 | persistent=False) as grad_t: 80 | grad_t.watch(time_input) 81 | network_output_single = self.DenseCoreNetwork.call_inference(inputs=inputs, **kwargs)[:, state:state + 1] 82 | 83 | network_output_t_single = grad_t.gradient(network_output_single, 84 | time_input, 85 | unconnected_gradients='zero') 86 | 87 | list_network_output.append(network_output_single) 88 | list_network_output_t.append(network_output_t_single) 89 | 90 | network_output = tf.concat(list_network_output, axis=1) 91 | network_output_t = tf.concat(list_network_output_t, axis=1) 92 | 93 | return network_output, network_output_t 94 | 95 | def update_test_data_with_prediction(self, test_data): 96 | X_testing = [test_data['time'], 97 | test_data['power']] 98 | 99 | prediction_split = self.predict(X_testing) 100 | network_states, network_states_t, network_physics = np.split(np.concatenate(prediction_split, axis=1), 101 | indices_or_sections=3, 102 | axis=1) 103 | 104 | test_data['states_prediction'] = network_states 105 | test_data['states_t_prediction'] = network_states_t 106 | test_data['physics_prediction'] = network_physics 107 | 108 | return test_data 109 | 110 | 111 | class DenseCoreNetwork(tf.keras.models.Model): 112 | """ 113 | This constitutes the core neural network with the PINN model. It outputs the angle and frequency for each 114 | generator and laod based on the disturbance. Additionally a common time input represents the time instance that 115 | shall be predicted. 116 | """ 117 | 118 | def __init__(self, n_states, neurons_in_hidden_layer, output_offset): 119 | 120 | super(DenseCoreNetwork, self).__init__() 121 | 122 | self.input_normalisation = tf.keras.layers.experimental.preprocessing.Normalization(axis=1, 123 | name='input_normalisation', 124 | mean=np.array([0.162, 3.]), 125 | variance=np.array([ 126 | 0.022, 3.4])) 127 | 128 | self.hidden_layer_0 = tf.keras.layers.Dense(units=neurons_in_hidden_layer[0], 129 | activation=tf.keras.activations.tanh, 130 | use_bias=True, 131 | kernel_initializer=tf.keras.initializers.glorot_normal, 132 | bias_initializer=tf.keras.initializers.zeros, 133 | name='first_layer') 134 | self.layer_0_normalisation = tf.keras.layers.BatchNormalization(name='layer_0_normalisation', trainable=True) 135 | self.hidden_layer_1 = tf.keras.layers.Dense(units=neurons_in_hidden_layer[1], 136 | activation=tf.keras.activations.tanh, 137 | use_bias=True, 138 | kernel_initializer=tf.keras.initializers.glorot_normal, 139 | bias_initializer=tf.keras.initializers.zeros, 140 | name='hidden_layer_1') 141 | 142 | self.layer_1_normalisation = tf.keras.layers.BatchNormalization(name='layer_1_normalisation', trainable=True) 143 | 144 | self.dense_output_layer = tf.keras.layers.Dense(units=n_states, 145 | activation=tf.keras.activations.linear, 146 | use_bias=True, 147 | kernel_initializer=tf.keras.initializers.glorot_normal, 148 | bias_initializer=tf.constant_initializer(-output_offset), 149 | name='output_layer') 150 | 151 | def call(self, inputs, training=None, mask=None): 152 | concatenated_input = tf.concat(inputs, axis=1, name='input_concatenation') 153 | 154 | hidden_layer_0_input = self.input_normalisation(concatenated_input) 155 | hidden_layer_0_output = self.hidden_layer_0(hidden_layer_0_input) 156 | hidden_layer_1_input = self.layer_0_normalisation(hidden_layer_0_output) 157 | hidden_layer_1_output = self.hidden_layer_1(hidden_layer_1_input) 158 | output_layer_input = self.layer_1_normalisation(hidden_layer_1_output) 159 | network_output = self.dense_output_layer(output_layer_input) 160 | 161 | return network_output 162 | 163 | def call_inference(self, inputs, training=None, mask=None): 164 | concatenated_input = tf.concat(inputs, axis=1, name='input_concatenation') 165 | 166 | hidden_layer_0_input = self.input_normalisation(concatenated_input, training=False) 167 | hidden_layer_0_output = self.hidden_layer_0(hidden_layer_0_input) 168 | hidden_layer_1_input = self.layer_0_normalisation(hidden_layer_0_output, training=False) 169 | hidden_layer_1_output = self.hidden_layer_1(hidden_layer_1_input) 170 | output_layer_input = self.layer_1_normalisation(hidden_layer_1_output, training=False) 171 | network_output = self.dense_output_layer(output_layer_input) 172 | 173 | return network_output 174 | 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Transient Stability Analysis with Physics-Informed Neural Networks 2 | 3 | This repository is the official implementation of [Transient Stability Analysis with Physics-Informed Neural Networks](https://arxiv.org/abs/2106.13638). 4 | 5 | ## Environment 6 | 7 | To install and activate the environment using conda run: 8 | 9 | ```setup 10 | conda env create -f environment.yml 11 | conda activate pinns_tf_2_4 12 | ``` 13 | 14 | ## Code structure 15 | The code is structured in the following way: 16 | - `train_model.py` contains the entire workflow to train a single model 17 | - `power_system_functions.py` sets up the power system model, including the parameters and the relevant state equations for simulations and the physics evaluations within the PINN. 18 | - `PINN.py` defines the neural network model that inherits from the class `tensorflow.keras.models.Model` 19 | - `create_data.py` creates a database of trajectories that is used in the selection of the training, validation, and test data. Needs to be run only once. 20 | - `dataset_handling.py` prepares the data by splitting them and provide the correct format. 21 | - `setup_and_run` provides a wrapper to setup and run multiple training processes in parallel. 22 | 23 | ## Folder structure 24 | The directory for the storage of all data should contain the following folders and needs to be defined in `train_model.py`, `create_data.py`, and `setup_and_run`: 25 | - `datasets` 26 | - `logs` 27 | - `result_datasets` 28 | - `model_weights` 29 | - `quantiles` 30 | - `setup_tables` 31 | 32 | ## Citation 33 | 34 | @misc{stiasny2021transient, 35 | title={Transient Stability Analysis with Physics-Informed Neural Networks}, 36 | author={Jochen Stiasny and Georgios S. Misyris and Spyros Chatzivasileiadis}, 37 | year={2021}, 38 | eprint={2106.13638}, 39 | archivePrefix={arXiv}, 40 | primaryClass={cs.LG} 41 | } 42 | 43 | ## Related work 44 | 45 | The concept of PINNs was introduced by Raissi et al. (https://maziarraissi.github.io/PINNs/) and adapted to power systems by Misyris et al. (https://github.com/gmisy/Physics-Informed-Neural-Networks-for-Power-Systems). The presented code is inspired by these two sources. 46 | -------------------------------------------------------------------------------- /create_data.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import numpy as np 3 | import pathlib 4 | import pickle 5 | from scipy import integrate 6 | import time 7 | 8 | from power_system_functions import create_system_matrices, compute_equilibrium_state, ode_right_hand_side_solve, create_power_system 9 | 10 | 11 | # ----------------------------- 12 | # functions for simulating the specified trajectory and creating a dataset by creating data points on a pre-defined 13 | # grid structure. 14 | # Needs be run only once if the dataset is kept constant. 15 | # ----------------------------- 16 | 17 | def input_data_initialised(n_ops, power_system): 18 | """ 19 | Standard initialisation 20 | """ 21 | time_zeros = np.zeros((n_ops, 1)) 22 | power_zeros = np.zeros((n_ops, power_system['n_buses'])) 23 | states_initial = np.zeros((n_ops, power_system['n_states'])) 24 | 25 | states_results_zeros = np.zeros((n_ops, power_system['n_states'])) 26 | states_t_results_zeros = np.zeros((n_ops, power_system['n_states'])) 27 | data_type_zeros = np.zeros((n_ops, power_system['n_states'])) 28 | 29 | data_initialised = {'time': time_zeros, 30 | 'power': power_zeros, 31 | 'states_initial': states_initial, 32 | 'states_results': states_results_zeros, 33 | 'states_t_results': states_t_results_zeros, 34 | 'data_type': data_type_zeros} 35 | 36 | return data_initialised 37 | 38 | 39 | def create_training_data(n_time_steps, n_power_steps, power_min, power_max, t_settle_disturbance, t_short_circuit, 40 | bus_disturbance, power_system): 41 | data_ops = input_data_initialised(n_ops=n_power_steps, 42 | power_system=power_system) 43 | power_ops = np.zeros((n_power_steps, power_system['n_buses'])) 44 | power_ops[:, bus_disturbance] = np.linspace(power_min, power_max, n_power_steps) 45 | data_ops.update(time=np.ones((n_power_steps, 1)) * t_settle_disturbance, 46 | power=power_ops) 47 | 48 | x_equilibrium_undisturbed = compute_equilibrium_state(power_system, 49 | u_disturbance=None, 50 | slack_bus=power_system['slack_bus_idx'], 51 | system_case='normal') 52 | 53 | data_ops.update(states_initial=np.repeat(x_equilibrium_undisturbed.T, repeats=n_power_steps, axis=0), 54 | data_type=np.ones((n_power_steps, power_system['n_states']))) 55 | 56 | data_ops = evaluate_ops(data_ops, 'normal', power_system) 57 | 58 | data_ops.update(states_initial=data_ops['states_results'], 59 | time=np.ones((n_power_steps, 1)) * t_short_circuit) 60 | 61 | data_ops = evaluate_ops(data_ops, 'short_circuit', power_system) 62 | 63 | shorted_bus_angles = data_ops['states_results'][:, 9:10] 64 | shorted_bus_angle_offset = np.floor((shorted_bus_angles + np.pi) / (2 * np.pi)) * 2 * np.pi 65 | states_results = data_ops['states_results'] 66 | states_results[:, 9:10] = states_results[:, 9:10] - shorted_bus_angle_offset 67 | 68 | t_max = power_system['t_max'] 69 | data_ops.update(states_initial=states_results, 70 | time=np.ones((n_power_steps, 1)) * t_max) 71 | 72 | start_time = time.time() 73 | data_ops = evaluate_op_trajectory(data_ops, 74 | n_time_steps=n_time_steps, 75 | system_case='line_tripped', 76 | power_system=power_system) 77 | print(time.time() - start_time) 78 | 79 | data_ops = calculate_data_ode_right_hand_side(data_ops, 'line_tripped', power_system) 80 | 81 | return data_ops 82 | 83 | 84 | def calculate_data_ode_right_hand_side(data_ops, system_case, power_system): 85 | states_results = data_ops['states_results'] 86 | A, B, C, D, F, G, u_0, x_0 = create_system_matrices(power_system=power_system, case=system_case) 87 | 88 | u_disturbance = data_ops['power'] @ G.T 89 | u = u_0.T + u_disturbance 90 | 91 | solver_func = functools.partial(ode_right_hand_side_solve, A=A, B=B, C=C, D=D, F=F) 92 | 93 | solver_results = map(solver_func, 94 | data_ops['time'], 95 | states_results, 96 | u) 97 | 98 | list_solver_results = list(solver_results) 99 | 100 | states_t_results = np.concatenate([single_solver_result.reshape((1, -1)) for single_solver_result in 101 | list_solver_results], 102 | axis=0) 103 | 104 | data_ops.update(states_t_results=states_t_results) 105 | 106 | return data_ops 107 | 108 | 109 | def evaluate_ops(data_ops, system_case, power_system): 110 | states_initial = data_ops['states_initial'] 111 | t_span = np.concatenate([data_ops['time'] * 0, 112 | data_ops['time']], axis=1) 113 | 114 | A, B, C, D, F, G, u_0, x_0 = create_system_matrices(power_system=power_system, case=system_case) 115 | 116 | u_disturbance = data_ops['power'] @ G.T 117 | u = u_0.T + u_disturbance 118 | solver_func = functools.partial(solve_ode, A=A, B=B, C=C, D=D, F=F) 119 | 120 | solver_results = map(solver_func, 121 | t_span, 122 | data_ops['time'], 123 | states_initial, 124 | u) 125 | 126 | list_solver_results = list(solver_results) 127 | 128 | states_results = np.concatenate([single_solver_result.T for single_solver_result in list_solver_results], axis=0) 129 | 130 | data_ops.update(states_results=states_results) 131 | 132 | return data_ops 133 | 134 | 135 | def solve_ode(t_span, 136 | t_eval, 137 | states_initial, 138 | u, A, B, C, D, F): 139 | ode_solution = integrate.solve_ivp(ode_right_hand_side_solve, 140 | t_span=t_span, 141 | y0=states_initial.flatten(), 142 | args=[u, A, B, C, D, F], 143 | t_eval=t_eval, 144 | rtol=1e-5) 145 | 146 | return ode_solution.y 147 | 148 | 149 | def evaluate_op_trajectory(data_ops, n_time_steps, system_case, power_system): 150 | n_ops = data_ops['time'].shape[0] 151 | t_max = power_system['t_max'] 152 | 153 | t_span = np.concatenate([np.zeros(data_ops['time'].shape), 154 | np.ones(data_ops['time'].shape) * t_max], axis=1) 155 | t_eval_vector = np.linspace(start=0, stop=t_max, num=n_time_steps).reshape((1, -1)) 156 | t_eval = np.repeat(t_eval_vector, repeats=n_ops, axis=0) 157 | 158 | states_initial = data_ops['states_initial'] 159 | A, B, C, D, F, G, u_0, x_0 = create_system_matrices(power_system=power_system, case=system_case) 160 | 161 | u_disturbance = data_ops['power'] @ G.T 162 | u = u_0.T + u_disturbance 163 | 164 | solver_func = functools.partial(solve_ode, A=A, B=B, C=C, D=D, F=F) 165 | 166 | solver_results = map(solver_func, 167 | t_span, 168 | t_eval, 169 | states_initial, 170 | u) 171 | 172 | list_solver_results = list(solver_results) 173 | 174 | states_results = np.concatenate([single_solver_result.T for single_solver_result in list_solver_results], axis=0) 175 | 176 | data_ops.update(time=t_eval.flatten().reshape((-1, 1)), 177 | power=np.repeat(data_ops['power'], repeats=n_time_steps, axis=0), 178 | states_initial=np.repeat(data_ops['states_initial'], repeats=n_time_steps, axis=0), 179 | states_results=states_results, 180 | data_type=np.repeat(data_ops['data_type'], repeats=n_time_steps, axis=0)) 181 | 182 | return data_ops 183 | 184 | 185 | if __name__ == '__main__': 186 | power_system = create_power_system() 187 | 188 | t_max = 2.0 189 | power_system['t_max'] = t_max 190 | t_settle_disturbance = 5.0 191 | n_time_steps = 1001 192 | n_power_steps = 121 193 | power_min = 0.0 194 | power_max = 6.0 195 | bus_disturbance = 4 196 | t_short_circuit = 0.05 197 | 198 | training_data = create_training_data(n_time_steps=n_time_steps, 199 | n_power_steps=n_power_steps, 200 | power_min=power_min, 201 | power_max=power_max, 202 | t_settle_disturbance=t_settle_disturbance, 203 | t_short_circuit=t_short_circuit, 204 | bus_disturbance=bus_disturbance, 205 | power_system=power_system) 206 | 207 | # TODO: Define the path to store all relevant data 208 | # 209 | # directory_data: pathlib.Path = pathlib.Path('Here_goes_your_path') 210 | raise Exception('Please specify directory_data, then delete this Exception.') 211 | 212 | with open(directory_data / 'datasets' / 'complete_dataset.pickle', 'wb') as f: 213 | pickle.dump(training_data, f) 214 | -------------------------------------------------------------------------------- /dataset_handling.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | # ----------------------------- 4 | # Functions for handling the dataset, namely filtering, dividing, and preparing it. 5 | # ----------------------------- 6 | 7 | 8 | def filter_dataset(dataset, filter_indices): 9 | dataset_copy = dataset.copy() 10 | for key in dataset_copy.keys(): 11 | dataset_copy[key] = dataset_copy[key][filter_indices, :] 12 | 13 | return dataset_copy 14 | 15 | 16 | def divide_dataset(dataset, 17 | n_power_steps_training, 18 | n_time_steps_training): 19 | power_min = 0.0 20 | power_max = 6.0 21 | time_min = 0.0 22 | time_max = 2.0 23 | bus_disturbance = 4 24 | 25 | n_power_steps_collocation = 25 26 | n_time_steps_collocation = 41 27 | 28 | # ------ data points including initial conditions on collocation trajectories ----------------- 29 | linspace_power_steps = np.around(np.linspace(power_min, power_max, n_power_steps_training), decimals=5) 30 | linspace_time_steps = np.around(np.linspace(time_min, time_max, n_time_steps_training), decimals=5) 31 | 32 | indices_power_steps = np.isin(np.around(dataset['power'][:, bus_disturbance], decimals=5), linspace_power_steps) 33 | indices_time_steps = np.isin(np.around(dataset['time'][:, 0], decimals=5), linspace_time_steps) 34 | 35 | linspace_power_steps_initial = np.around(np.linspace(power_min, power_max, n_power_steps_collocation), decimals=5) 36 | 37 | indices_power_steps_initial = np.isin(np.around(dataset['power'][:, bus_disturbance], decimals=5), 38 | linspace_power_steps_initial) 39 | indices_time_steps_initial = np.isin(np.around(dataset['time'][:, 0], decimals=5), 0) 40 | filter_indices_training_initial = np.logical_and(indices_power_steps_initial, indices_time_steps_initial) 41 | 42 | filter_indices_training = np.logical_or(np.logical_and(indices_power_steps, indices_time_steps), 43 | filter_indices_training_initial) 44 | 45 | # ------ collocation points ----------------- 46 | linspace_power_steps = np.around(np.linspace(power_min, power_max, n_power_steps_collocation), decimals=5) 47 | linspace_time_steps = np.around(np.linspace(time_min, time_max, n_time_steps_collocation), decimals=5) 48 | 49 | indices_power_steps = np.isin(np.around(dataset['power'][:, bus_disturbance], decimals=5), linspace_power_steps) 50 | indices_time_steps = np.isin(np.around(dataset['time'][:, 0], decimals=5), linspace_time_steps) 51 | filter_indices_collocation = np.logical_and(np.logical_and(indices_power_steps, indices_time_steps), 52 | np.logical_not(filter_indices_training)) 53 | 54 | # ------ validation data ----------------- 55 | linspace_power_steps = np.around(np.linspace(power_min, power_max, n_power_steps_collocation) + 0.10000, decimals=5) 56 | linspace_time_steps = np.around(np.linspace(time_min, time_max, n_time_steps_collocation) + 0.02400, decimals=5) 57 | 58 | indices_power_steps = np.isin(np.around(dataset['power'][:, bus_disturbance], decimals=5), linspace_power_steps) 59 | indices_time_steps = np.isin(np.around(dataset['time'][:, 0], decimals=5), linspace_time_steps) 60 | filter_indices_validation = np.logical_and(indices_power_steps, indices_time_steps) 61 | 62 | # ------ testing data points ----------------- 63 | linspace_power_steps = np.around(np.linspace(power_min, power_max, 61), decimals=5) 64 | linspace_time_steps = np.around(np.linspace(time_min, time_max, 201), decimals=5) 65 | 66 | indices_power_steps = np.isin(np.around(dataset['power'][:, bus_disturbance], decimals=5), linspace_power_steps) 67 | indices_time_steps = np.isin(np.around(dataset['time'][:, 0], decimals=5), linspace_time_steps) 68 | filter_indices_testing = np.logical_and(indices_power_steps, indices_time_steps) 69 | 70 | if sum(filter_indices_training) != n_power_steps_training * (n_time_steps_training - 1) + n_power_steps_collocation: 71 | raise Exception('Error in training data filtering') 72 | else: 73 | print(f'Filtered {sum(filter_indices_training)} training data points.') 74 | 75 | if sum(filter_indices_collocation) != n_power_steps_collocation * n_time_steps_collocation - sum( 76 | filter_indices_training): 77 | raise Exception('Error in collocation data filtering') 78 | else: 79 | print(f'Filtered {sum(filter_indices_collocation)} collocation data points.') 80 | 81 | if sum(filter_indices_validation) != (n_power_steps_collocation - 1) * (n_time_steps_collocation - 1): 82 | raise Exception('Error in validation data filtering') 83 | else: 84 | print(f'Filtered {sum(filter_indices_validation)} validation data points.') 85 | 86 | if sum(filter_indices_testing) != 61 * 201: 87 | raise Exception('Error in test data filtering') 88 | else: 89 | print(f'Filtered {sum(filter_indices_testing)} test data points.') 90 | 91 | training_data_pure = filter_dataset(dataset=dataset, filter_indices=filter_indices_training) 92 | collocation_data = filter_dataset(dataset=dataset, filter_indices=filter_indices_collocation) 93 | validation_data = filter_dataset(dataset=dataset, filter_indices=filter_indices_validation) 94 | testing_data = filter_dataset(dataset=dataset, filter_indices=filter_indices_testing) 95 | 96 | collocation_data['data_type'] = collocation_data['data_type'] * 0 97 | 98 | training_data = training_data_pure.copy() 99 | for key in training_data.keys(): 100 | training_data[key] = np.concatenate([training_data_pure[key], 101 | collocation_data[key]], 102 | axis=0) 103 | 104 | return training_data, training_data_pure, validation_data, testing_data 105 | 106 | 107 | def prepare_data(dataset, n_states): 108 | X_dataset = [dataset['time'], 109 | dataset['power']] 110 | 111 | y_dataset = np.split(dataset['states_results'], indices_or_sections=n_states, axis=1) + \ 112 | np.split(dataset['states_t_results'], indices_or_sections=n_states, axis=1) + \ 113 | [np.zeros((dataset['states_initial'].shape[0], 1))] * n_states 114 | 115 | return X_dataset, y_dataset 116 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: pinns_tf_2_4 2 | channels: 3 | - defaults 4 | dependencies: 5 | - joblib=1.0.1 6 | - pandas=1.2.4 7 | - pip=21.1.1 8 | - python=3.7.10 9 | - python-dateutil=2.8.1 10 | - pytz=2021.1 11 | - scipy=1.6.2 12 | - setuptools=52.0.0 13 | - six=1.15.0 14 | - sqlite=3.35.4 15 | - tk=8.6.10 16 | - wheel=0.36.2 17 | - xz=5.2.5 18 | - zlib=1.2.11 19 | - pip: 20 | - absl-py==0.12.0 21 | - astunparse==1.6.3 22 | - cachetools==4.2.2 23 | - chardet==4.0.0 24 | - flatbuffers==1.12 25 | - gast==0.3.3 26 | - google-auth==1.30.1 27 | - google-auth-oauthlib==0.4.4 28 | - google-pasta==0.2.0 29 | - grpcio==1.32.0 30 | - h5py==2.10.0 31 | - idna==2.10 32 | - importlib-metadata==4.5.0 33 | - keras-preprocessing==1.1.2 34 | - markdown==3.3.4 35 | - numpy==1.19.5 36 | - oauthlib==3.1.1 37 | - opt-einsum==3.3.0 38 | - protobuf==3.17.2 39 | - pyasn1==0.4.8 40 | - pyasn1-modules==0.2.8 41 | - requests==2.25.1 42 | - requests-oauthlib==1.3.0 43 | - rsa==4.7.2 44 | - tensorboard==2.5.0 45 | - tensorboard-data-server==0.6.1 46 | - tensorboard-plugin-wit==1.8.0 47 | - tensorflow==2.4.0 48 | - tensorflow-estimator==2.4.0 49 | - termcolor==1.1.0 50 | - typing-extensions==3.7.4.3 51 | - urllib3==1.26.5 52 | - werkzeug==2.0.1 53 | - wrapt==1.12.1 54 | - zipp==3.4.1 55 | -------------------------------------------------------------------------------- /power_system_functions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.optimize import fsolve 3 | 4 | # ----------------------------- 5 | # General functions that define the power system model and the state update equations, as well as functions used in 6 | # the simulation of the trajectories. 7 | # ----------------------------- 8 | 9 | def create_power_system(): 10 | n_buses = 6 11 | n_generators = 4 12 | n_non_generators = n_buses - n_generators 13 | n_states = 2 * n_generators + 1 * n_non_generators 14 | 15 | omega_0 = 2 * np.pi * 60 16 | 17 | output_scaling = np.ones((n_states, 1)) 18 | output_scaling[n_generators:2 * n_generators] = omega_0 19 | 20 | output_offset = np.zeros((n_states, 1)) 21 | output_offset[n_generators:2 * n_generators] = -omega_0 22 | 23 | H_generators = np.array([58.5, 58.5, 55.575, 55.575]) 24 | D_generators = 0.0 * np.ones(n_generators) 25 | 26 | D_non_generators = np.array([0.1, 0.2]) * 2 27 | 28 | P_load_set_point = np.array([-9.67, -17.67]) 29 | P_generator_set_point = np.array([7, 7, 6.34, 7]) 30 | P_set_point = np.hstack([P_generator_set_point, P_load_set_point]) 31 | 32 | P_disturbance = np.zeros(n_buses) 33 | slack_bus_idx = 2 34 | 35 | V_magnitude = np.array([1.0300, 36 | 1.0100, 37 | 1.0300, 38 | 1.0100, 39 | 0.9610, 40 | 0.9714]) 41 | 42 | # short circuit at bus 9 43 | V_magnitude_short_circuit = np.array([1.0300, 44 | 1.0100, 45 | 1.0300, 46 | 1.0100, 47 | 0.9610, 48 | 0.000]) 49 | 50 | B_susceptance = np.array([7.8461, 51 | 7.8461, 52 | 12.9499, 53 | 32.5581, 54 | 12.9499, 55 | 32.5581, 56 | 9.0982]) 57 | 58 | # trip one line between bus 10 and 11 (line index 10), susceptance halfed 59 | B_susceptance_line_tripped = np.array([7.8461, 60 | 7.8461, 61 | 12.9499, 62 | 32.5581, 63 | 12.9499, 64 | 32.5581, 65 | 6.0655]) 66 | 67 | b_from = np.array([0, 68 | 2, 69 | 0, 70 | 1, 71 | 2, 72 | 3, 73 | 4], dtype=int) 74 | 75 | b_to = np.array([1, 76 | 3, 77 | 4, 78 | 4, 79 | 5, 80 | 5, 81 | 5], dtype=int) 82 | 83 | V_i_V_j_B_full = V_magnitude[b_from] * V_magnitude[b_to] * B_susceptance 84 | V_i_V_j_B_short_circuit = V_magnitude_short_circuit[b_from] * V_magnitude_short_circuit[b_to] * B_susceptance 85 | V_i_V_j_B_line_tripped = V_magnitude[b_from] * V_magnitude[b_to] * B_susceptance_line_tripped 86 | 87 | incidence_matrix = np.array([[1, -1, 0, 0, 0, 0], 88 | [0, 0, 1, -1, 0, 0], 89 | [1, 0, 0, 0, -1, 0], 90 | [0, 1, 0, 0, -1, 0], 91 | [0, 0, 1, 0, 0, -1], 92 | [0, 0, 0, 1, 0, -1], 93 | [0, 0, 0, 0, 1, -1]]) 94 | 95 | t_max = 2.0 96 | 97 | system_parameters = {'n_buses': n_buses, 98 | 'n_generators': n_generators, 99 | 'n_non_generators': n_non_generators, 100 | 'n_states': n_states, 101 | 'slack_bus_idx': slack_bus_idx, 102 | 'H_generators': H_generators, 103 | 'D_generators': D_generators, 104 | 'omega_0': omega_0, 105 | 'output_scaling': output_scaling, 106 | 'D_non_generators': D_non_generators, 107 | 'P_disturbance': P_disturbance, 108 | 'P_set_point': P_set_point, 109 | 'V_i_V_j_B_full': V_i_V_j_B_full, 110 | 'V_i_V_j_B_short_circuit': V_i_V_j_B_short_circuit, 111 | 'V_i_V_j_B_line_tripped': V_i_V_j_B_line_tripped, 112 | 'incidence_matrix': incidence_matrix, 113 | 't_max': t_max, 114 | 'output_offset': output_offset} 115 | 116 | print('Successfully created the reduced Kundur 2 area system (6 buses, 4 generators)!') 117 | 118 | return system_parameters 119 | 120 | 121 | def create_system_matrices(power_system, case='normal'): 122 | 123 | n_g = power_system['n_generators'] 124 | n_b = power_system['n_buses'] 125 | n_d = n_b - n_g 126 | H_total = sum(power_system['H_generators']) 127 | 128 | # -------------------------------- 129 | # A-matrix 130 | A_11 = np.zeros((n_g, n_g)) 131 | A_12 = (np.eye(n_g) * H_total - np.repeat(power_system['H_generators'].reshape((1, n_g)), repeats=n_g, 132 | axis=0)) / H_total 133 | A_21 = np.zeros((n_g, n_g)) 134 | A_22 = np.diag(-power_system['omega_0'] / (2 * power_system['H_generators']) * ( 135 | power_system['D_generators'] + power_system['K_g_generators'])) 136 | 137 | A_13 = np.zeros((n_g, n_d)) 138 | A_23 = np.zeros((n_g, n_d)) 139 | A_31 = np.zeros((n_d, n_g)) 140 | A_32 = np.zeros((n_d, n_g)) 141 | A_33 = np.zeros((n_d, n_d)) 142 | 143 | A = np.block([ 144 | [A_11, A_12, A_13], 145 | [A_21, A_22, A_23], 146 | [A_31, A_32, A_33] 147 | ]) 148 | 149 | # -------------------------------- 150 | # F-matrix 151 | F_11 = np.zeros((n_g, n_g)) 152 | F_21 = np.diag(-power_system['omega_0'] / (2 * power_system['H_generators'])) 153 | 154 | F_12 = np.zeros((n_g, n_d)) 155 | F_22 = np.zeros((n_g, n_d)) 156 | F_31 = np.zeros((n_d, n_g)) 157 | F_32 = np.diag(-1 / power_system['D_non_generators']) 158 | 159 | F = np.block([ 160 | [F_11, F_12], 161 | [F_21, F_22], 162 | [F_31, F_32] 163 | ]) 164 | 165 | # -------------------------------- 166 | # B-matrix 167 | # B_11 = -np.ones((n_g, 1)) 168 | B_11 = np.zeros((n_g, 1)) 169 | B_12 = np.zeros((n_g, n_g)) 170 | B_21 = np.reshape(power_system['omega_0'] / (2 * power_system['H_generators']) * power_system[ 171 | 'K_g_generators'], (n_g, 1)) 172 | B_22 = np.diag(power_system['omega_0'] / (2 * power_system['H_generators'])) 173 | 174 | B_13 = np.zeros((n_g, n_d)) 175 | B_23 = np.zeros((n_g, n_d)) 176 | B_31 = np.zeros((n_d, 1)) 177 | B_32 = np.zeros((n_d, n_g)) 178 | B_33 = np.diag(1 / power_system['D_non_generators']) 179 | 180 | B = np.block([ 181 | [B_11, B_12, B_13], 182 | [B_21, B_22, B_23], 183 | [B_31, B_32, B_33] 184 | ]) 185 | 186 | # -------------------------------- 187 | # U-matrix 188 | U_11 = np.eye(n_g) 189 | U_12 = np.zeros((n_g, n_g)) 190 | U_13 = np.zeros((n_g, n_d)) 191 | 192 | U_21 = np.zeros((n_d, n_g)) 193 | U_22 = np.zeros((n_d, n_g)) 194 | U_23 = np.eye(n_d) 195 | 196 | U = np.block([ 197 | [U_11, U_12, U_13], 198 | [U_21, U_22, U_23] 199 | ]) 200 | 201 | C = power_system['incidence_matrix'] @ U 202 | 203 | if case == 'normal': 204 | D = power_system['incidence_matrix'].T @ np.diag(power_system['V_i_V_j_B_full']) 205 | elif case == 'short_circuit': 206 | D = power_system['incidence_matrix'].T @ np.diag(power_system['V_i_V_j_B_short_circuit']) 207 | elif case == 'line_tripped': 208 | D = power_system['incidence_matrix'].T @ np.diag(power_system['V_i_V_j_B_line_tripped']) 209 | else: 210 | raise Exception('Specify a valid case') 211 | 212 | # adjustment of u to accommodate power disturbance input 213 | G = np.block([ 214 | [np.zeros((1, n_b))], 215 | [np.eye(n_b)] 216 | ]) 217 | 218 | # set point of the power before any disturbance 219 | u_0 = np.hstack([power_system['omega_0'], 220 | power_system['P_set_point'][:n_g] + power_system['D_generators'] * power_system['omega_0'], 221 | power_system['P_set_point'][n_g:]]).reshape((-1, 1)) 222 | 223 | # initial value for equilibrium computation 224 | x_0 = np.hstack([np.zeros(n_g), 225 | np.ones(n_g) * power_system['omega_0'], 226 | np.zeros(n_d)]).reshape((-1, 1)) 227 | 228 | return A, B, C, D, F, G, u_0, x_0 229 | 230 | 231 | def compute_equilibrium_state(power_system, u_disturbance=None, slack_bus=None, system_case='normal'): 232 | A, B, C, D, F, G, u_0, x_0 = create_system_matrices(power_system=power_system, case=system_case) 233 | 234 | if u_disturbance is not None: 235 | u = u_0 + u_disturbance 236 | else: 237 | u = u_0 238 | 239 | if system_case == 'short_circuit': 240 | raise Exception('No equilibrium will be found for short circuit configurations.') 241 | 242 | x_equilibrium, info_dict, ier, mesg = fsolve(ode_right_hand_side, 243 | x0=x_0, 244 | args=(u, A, B, C, D, F, slack_bus), 245 | xtol=1.49012e-08, 246 | full_output=True) 247 | 248 | if not np.allclose(info_dict['fvec'], 249 | np.zeros(info_dict['fvec'].shape), 250 | atol=1e-08): 251 | raise Exception(f'No equilibrium found. Error message {mesg}') 252 | else: 253 | return x_equilibrium.reshape((-1, 1)) 254 | 255 | 256 | def ode_right_hand_side(x, u, A, B, C, D, F, slack=None): 257 | x_vector = np.reshape(x, (-1, 1)) 258 | if slack is not None: 259 | x_vector[slack] = 0 260 | 261 | FCX = D @ np.sin(C @ x_vector) 262 | 263 | dx = A @ x_vector + F @ FCX + B @ u 264 | return dx[:, 0] 265 | 266 | 267 | def ode_right_hand_side_solve(t, x, u, A, B, C, D, F): 268 | x_vector = np.reshape(x, (-1, 1)) 269 | u_vector = np.reshape(u, (-1, 1)) 270 | 271 | FCX = D @ np.sin(C @ x_vector) 272 | 273 | dx = A @ x_vector + F @ FCX + B @ u_vector 274 | return dx[:, 0] -------------------------------------------------------------------------------- /setup_and_run.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python3 2 | import time 3 | import hashlib 4 | import pandas as pd 5 | import numpy as np 6 | import itertools 7 | import pickle 8 | import pathlib 9 | import tensorflow as tf 10 | import multiprocessing as mp 11 | from train_model import train_model 12 | 13 | tf.config.threading.set_inter_op_parallelism_threads(num_threads=1) 14 | tf.config.threading.set_intra_op_parallelism_threads(num_threads=1) 15 | 16 | # ----------------------------- 17 | # Wrapper script for running multiple variations in parallel. The definition for each training is stored in a 18 | # 'setup_table' and contains the unique simulation IDs to identify each training run. 19 | # ----------------------------- 20 | 21 | 22 | def setup_and_run(): 23 | tf.config.threading.set_inter_op_parallelism_threads(num_threads=1) 24 | tf.config.threading.set_intra_op_parallelism_threads(num_threads=1) 25 | 26 | # TODO: Define the path to store all relevant data 27 | # directory_data: pathlib.Path = pathlib.Path('Here_goes_your_path') 28 | raise Exception('Please specify directory_data, then delete this Exception.') 29 | 30 | current_time = int(time.time() * 1000) 31 | setup_id = hashlib.md5(str(current_time).encode()) 32 | 33 | setup_id_path = directory_data / 'setup_tables' / f'setupID_{setup_id.hexdigest()}.pickle' 34 | 35 | setup_table_names = ['NN_type', 36 | 'data_points', 37 | 'seed_tensorflow'] 38 | 39 | # to be usable in "itertools.product(*parameters)" 40 | NN_type = ['NN', 'dtNN', 'PINN'] 41 | data_points = [[5, 5], [5, 9], [5, 21], [9, 9], [13, 9], [25, 41]] 42 | np.random.seed(94589) 43 | seed_tensorflow = np.random.randint(0, 1000000, 20).tolist() 44 | 45 | parameters = [NN_type, 46 | data_points, 47 | seed_tensorflow] 48 | 49 | setup_table = pd.DataFrame(itertools.product(*parameters), columns=setup_table_names) 50 | 51 | setup_table.insert(0, "setupID", setup_id.hexdigest()) 52 | n_power_steps_training = [pair[0] for pair in setup_table['data_points']] 53 | n_time_steps_training = [pair[1] for pair in setup_table['data_points']] 54 | setup_table.insert(loc=3, column="n_power_steps_training", value=n_power_steps_training) 55 | setup_table.insert(loc=4, column="n_time_steps_training", value=n_time_steps_training) 56 | 57 | simulation_ids_unhashed = current_time + 1 + setup_table.index.values 58 | simulation_ids = [] 59 | for simulation_id in simulation_ids_unhashed: 60 | simulation_ids_hashed = hashlib.md5(str(simulation_id).encode()) 61 | simulation_ids.append(simulation_ids_hashed.hexdigest()) 62 | 63 | setup_table.insert(1, "simulation_id", simulation_ids) 64 | 65 | with open(setup_id_path, "wb") as f: 66 | pickle.dump(setup_table, f) 67 | 68 | print('Created setup table with %i entries' % setup_table.shape[0]) 69 | 70 | starmap_variables = [(simulation_id, NN_type, n_power_steps_training, n_time_steps_training, seed_tensorflow) for 71 | (simulation_id, NN_type, n_power_steps_training, n_time_steps_training, seed_tensorflow) in 72 | zip(setup_table['simulation_id'], setup_table['NN_type'], 73 | setup_table['n_power_steps_training'], 74 | setup_table['n_time_steps_training'], setup_table['seed_tensorflow'])] 75 | 76 | with mp.Pool(20) as pool: 77 | pool.starmap(train_model, starmap_variables) 78 | 79 | pass 80 | 81 | 82 | if __name__ == '__main__': 83 | setup_and_run() 84 | -------------------------------------------------------------------------------- /train_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pathlib 3 | import pickle 4 | import tensorflow as tf 5 | 6 | from dataset_handling import divide_dataset, prepare_data 7 | from power_system_functions import create_power_system 8 | from PINN import PinnModel 9 | 10 | 11 | def train_model(simulation_id, NN_type, n_power_steps_training, n_time_steps_training, seed_tensorflow): 12 | """ 13 | The core training routine. 14 | 15 | :param simulation_id: a unique idenfier, useful when running multiple training setups etc 16 | :param NN_type: One of the three types NN, dtNN, or PINN - loss function is adjusted accordingly 17 | :param n_power_steps_training: parameter to control the number of data points 18 | :param n_time_steps_training: parameter to control the number of data points 19 | :param seed_tensorflow: seed for initialisation of the weights 20 | """ 21 | 22 | # ----------------------------- 23 | # Defining the relevant directories 24 | # ----------------------------- 25 | 26 | # TODO: Define the path to store all relevant data 27 | # 28 | # directory_data: pathlib.Path = pathlib.Path('Here_goes_your_path') 29 | 30 | raise Exception('Please specify directory_data, then delete this Exception.') 31 | 32 | directory_logging = directory_data / 'logs' / simulation_id 33 | 34 | directory_results = directory_data / 'result_datasets' 35 | 36 | directory_model_weights = directory_data / 'model_weights' 37 | 38 | directory_quantile = directory_data / 'quantiles' 39 | 40 | # ----------------------------- 41 | # Simple type check 42 | # ----------------------------- 43 | 44 | if type(simulation_id) is not str: 45 | raise Exception('Provide simulation_id as string.') 46 | 47 | if type(NN_type) is not str: 48 | raise Exception('Provide NN_type as string.') 49 | 50 | if type(n_power_steps_training) is not int: 51 | raise Exception('Provide n_power_steps_training as integer.') 52 | 53 | if type(n_time_steps_training) is not int: 54 | raise Exception('Provide n_time_steps_training as integer.') 55 | 56 | if type(seed_tensorflow) is not int: 57 | raise Exception('Provide seed_tensorflow as integer.') 58 | 59 | # ----------------------------- 60 | # Define the power system, here, by default the investigated Kundur two area system 61 | # ----------------------------- 62 | 63 | power_system = create_power_system() 64 | n_states = power_system['n_states'] 65 | 66 | # ----------------------------- 67 | # Basic NN and training parameters and logging setup 68 | # ----------------------------- 69 | 70 | neurons_in_hidden_layer = [200, 200] 71 | epochs_total = 10000 72 | learning_rate_initial = 0.01 73 | learning_rate_decay = 0.99 74 | 75 | tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir=directory_logging, 76 | histogram_freq=1, 77 | profile_batch=0, 78 | write_graph=False, 79 | ) 80 | 81 | # ----------------------------- 82 | # instantiate model 83 | # ----------------------------- 84 | 85 | model = PinnModel(neurons_in_hidden_layer=neurons_in_hidden_layer, 86 | power_system=power_system, 87 | case='line_tripped', 88 | seed=seed_tensorflow) 89 | 90 | # ----------------------------- 91 | # load data and select data and collocation points 92 | # ----------------------------- 93 | with open(directory_data / 'datasets' / 'complete_dataset.pickle', "rb") as f: 94 | complete_data = pickle.load(f) 95 | 96 | training_data, training_data_pure, validation_data, testing_data = divide_dataset(dataset=complete_data, 97 | n_power_steps_training=n_power_steps_training, 98 | n_time_steps_training=n_time_steps_training) 99 | 100 | # ----------------------------- 101 | # setting of the loss weights, heuristic 102 | # ----------------------------- 103 | 104 | loss_weights_initial = np.array([1.0, 1.0, 1.0, 1.0, 105 | 2.0, 2.0, 2.0, 2.0, 106 | 1.0, 1.0, 107 | 0.5, 0.5, 0.5, 0.5, 108 | 0.04, 0.04, 0.04, 0.04, 109 | 0.12, 0.12, 110 | 1000.0, 1000.0, 1000.0, 1000.0, 111 | 5.0, 5.0, 5.0, 5.0, 112 | 3.0, 3.0]) 113 | 114 | factor_data_points = (5 * 9) / n_time_steps_training / n_power_steps_training * 5.0e+05 115 | 116 | loss_weights_combined = loss_weights_initial.copy() 117 | loss_weights_combined[20:30] = loss_weights_initial[20:30] / factor_data_points 118 | 119 | # ----------------------------- 120 | # NN type specific settings, primarily regarding the loss calculation 121 | # ----------------------------- 122 | if NN_type == 'NN': 123 | X_training, y_training = prepare_data(training_data_pure, n_states=n_states) 124 | sample_weights_static = np.split(training_data_pure['data_type'], indices_or_sections=n_states, axis=1) + \ 125 | np.split(training_data_pure['data_type'], indices_or_sections=n_states, axis=1) + \ 126 | np.split(np.ones(training_data_pure['data_type'].shape), indices_or_sections=n_states, 127 | axis=1) 128 | loss_weights_NN_type = np.hstack([np.ones(n_states), np.zeros(n_states), np.zeros(n_states)]) 129 | loss_weights_np = loss_weights_combined * loss_weights_NN_type 130 | patience = 1000 131 | elif NN_type == 'dtNN': 132 | X_training, y_training = prepare_data(training_data_pure, n_states=n_states) 133 | sample_weights_static = np.split(training_data_pure['data_type'], indices_or_sections=n_states, axis=1) + \ 134 | np.split(training_data_pure['data_type'], indices_or_sections=n_states, axis=1) + \ 135 | np.split(np.ones(training_data_pure['data_type'].shape), indices_or_sections=n_states, 136 | axis=1) 137 | loss_weights_NN_type = np.hstack([np.ones(n_states), np.ones(n_states), np.zeros(n_states)]) 138 | loss_weights_np = loss_weights_combined * loss_weights_NN_type 139 | patience = 1000 140 | elif NN_type == 'PINN': 141 | X_training, y_training = prepare_data(training_data, n_states=n_states) 142 | sample_weights_static = np.split(training_data['data_type'], indices_or_sections=n_states, axis=1) + \ 143 | np.split(training_data['data_type'], indices_or_sections=n_states, axis=1) + \ 144 | np.split(np.ones(training_data['data_type'].shape), indices_or_sections=n_states, 145 | axis=1) 146 | loss_weights_NN_type = np.hstack([np.ones(n_states), np.ones(n_states), np.ones(n_states)]) 147 | loss_weights_np = loss_weights_combined * loss_weights_NN_type 148 | patience = 2500 149 | 150 | else: 151 | raise Exception('Invalid NN_type.') 152 | 153 | # ----------------------------- 154 | # validation set preparation 155 | # ----------------------------- 156 | X_validation, y_validation = prepare_data(validation_data, n_states=n_states) 157 | validation_sample_weights = np.split(np.concatenate([np.ones((X_validation[0].shape[0], n_states)), 158 | np.zeros((X_validation[0].shape[0], n_states)), 159 | np.zeros((X_validation[0].shape[0], n_states))], axis=1), 160 | indices_or_sections=3*n_states, axis=1) 161 | 162 | # ----------------------------- 163 | # final training preparation 164 | # ----------------------------- 165 | learning_rate_scheduler = tf.keras.optimizers.schedules.ExponentialDecay( 166 | initial_learning_rate=learning_rate_initial, 167 | decay_rate=learning_rate_decay, 168 | decay_steps=100) 169 | 170 | mse = tf.keras.losses.MeanSquaredError(reduction=tf.losses.Reduction.SUM_OVER_BATCH_SIZE) 171 | 172 | model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate_scheduler), 173 | loss=[mse] * 3*n_states, 174 | loss_weights=loss_weights_np.tolist(), 175 | ) 176 | 177 | early_stopping_callback = tf.keras.callbacks.EarlyStopping( 178 | monitor='val_loss', min_delta=0, patience=patience, verbose=0, 179 | mode='auto', baseline=None, restore_best_weights=True 180 | ) 181 | 182 | # ----------------------------- 183 | # training without intermediate plotting 184 | # ----------------------------- 185 | history_epochs = model.fit(X_training, 186 | y_training, 187 | initial_epoch=model.epoch_count, 188 | epochs=model.epoch_count + epochs_total, 189 | batch_size=int(X_training[0].shape[0]), 190 | sample_weight=sample_weights_static, 191 | validation_data=(X_validation, y_validation, validation_sample_weights), 192 | validation_freq=1, 193 | verbose=0, 194 | shuffle=True, 195 | callbacks=[early_stopping_callback, tensorboard_callback]) 196 | model.epoch_count = model.epoch_count + epochs_total 197 | 198 | # ----------------------------- 199 | # save model weights 200 | # ----------------------------- 201 | model.set_weights(early_stopping_callback.best_weights) 202 | model.save_weights(filepath=directory_model_weights / f'weights_{simulation_id}.h5') 203 | 204 | # ----------------------------- 205 | # store test data for detailed analyses, comment if summary statistics are sufficient 206 | # ----------------------------- 207 | complete_data = model.update_test_data_with_prediction(test_data=complete_data) 208 | 209 | with open(directory_results / f'dataset_{simulation_id}.pickle', 'wb') as file_opener: 210 | pickle.dump(complete_data, file_opener) 211 | 212 | # ----------------------------- 213 | # error analysis 214 | # ----------------------------- 215 | error = np.square(complete_data['states_prediction'] - complete_data['states_results']) 216 | 217 | quantile_values = np.array([0.0, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 218 | 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 0.98, 0.99, 0.995, 219 | 0.998, 0.999, 1.000]) 220 | 221 | quantile_results = np.quantile(error, quantile_values, axis=0) 222 | mean_results = np.mean(error, axis=0) 223 | 224 | with open(directory_quantile / f'quantiles_{simulation_id}.pickle', 'wb') as file_opener: 225 | pickle.dump(quantile_results, file_opener) 226 | 227 | with open(directory_quantile / f'mean_{simulation_id}.pickle', 'wb') as file_opener: 228 | pickle.dump(mean_results, file_opener) 229 | 230 | # ----------------------------- 231 | # console output after training 232 | # ----------------------------- 233 | np.set_printoptions(precision=4) 234 | print(f'Simulation ID {simulation_id}:') 235 | print(f'MSE states : {mean_results}') 236 | print(f'max SE states : {np.max(error, axis=0)}') 237 | pass 238 | 239 | 240 | if __name__ == '__main__': 241 | train_model('testID', 'NN', 5, 9, 3215) 242 | --------------------------------------------------------------------------------