#!/usr/bin/env python # coding: utf-8 # # Introduction to pandapower's control module # The control module in pandapower allows you to **adjust, regulate and manipulate individual components** within a power grid. The possibilities here are many and varied and are purely up to your imagination. They range from simple controllers that make it possible, for example, to adjust and manipulate the entries of loads, feeders or storage units in the course of a time series simulation, to equipment-specific controllers such as tap changers, through to complex controllers that control the various flexibilities within a power grid. # # This tutorial is intended to help you gain a basic understanding of the control module and thus enable you to use the control module for your purposes. # ## Structure of the control module # Compared to the other components that make up a power grid, the **controller module is rather exotic**. This is because, unlike with all other components, we had to realize that it makes less sense here to start from the classic table structure and try to embed the controllers in a fixed structure. In other words, while **buses, lines, and so on always depend on fixed variables**, this does not apply to controllers: **controllers are extremely flexible**. On the one hand, this is their strength (the possibilities here are limitless), but on the other hand it means that you lose the clear table structure that you are familiar with from the other components you have already seen. # # However, this should not prevent you from using our control module as you wish. We have tried to make it as structured as possible so that you can quickly find your way around and easily create your own controllers. # # ### Network embedding of the controllers # # **Every controller that is integrated into a pandapower network is an object**. Each controller object is based on the abstract *Controller* class in basic_controller.py. A **controller always requires a network to which the controller is assigned**. # In[1]: import pandapower as pp net = pp.create_empty_network() # The basic controller only needs the network as an external variable: # In[2]: from pandapower.control.basic_controller import Controller # In[3]: basic_control = Controller(net) # In[4]: net # A look at the individual attributes reveals the elementary parts of each controller: # In[5]: basic_control.__dict__ # The *index* is the indexing within the network, so to speak, the place where you can find your controller under net.controller. The storage location is a pandas DataFrame. # # *matching_params* checks if the defined controller already exists and enables the user to remove already defined controllers with the same parameters. # In[6]: net.controller # You will find the index at the beginning of each line. This is assigned automatically, but can also be defined individually by you. You will also find further important information here: # # The information *in_service* gives you the option of **ignoring a controller** at any time without having to delete it. # # *order* and *level*, on the other hand, are best explained graphically. # Assuming you have 5 controllers: # # # # # These are now placed in a sequence according to their *level* and *order*. It is important to note that **the higher** the *level* and *order*, **the later** the controllers become active, i.e. controllers with a higher *level* and *order* are **more important as they can react** to the control of a previously executed controller. Using the example above, this would look like this: # # # # # Controllers **within a *level* must always all converge**. Controllers in **different *levels* are independent of each other**. An example would look like this. Suppose there are two level controllers that control the voltage of a certain node. One controller wants the voltage at this node to always be below 1 p.u., while the other wants exactly the opposite. Even though this example is unlikely to be found in reality, it illustrates the difference between *level* and *order* quite well. If both controllers were at the same *level*, the controller loop would never converge, as the conditions of both controllers could never be fulfilled. However, if one of the controllers were at a higher *level*, each individual controller loop would again find a solution without any problems, as these two controllers are independent of each other. One controller would, for example, push the voltage below 1 p.u. in its *level*, while the second would then reverse this in its *level*. # # ### Basic structure of each controller # # The most important functions of each controller are as follows: # - intialize_control # - control_step # - repair_control # - finalize_control # # Each of these four functions is ultimately relevant for the control process and describes the behaviour of the controller in the network. *initialize_control* is called at the **beginning, before the controller loops are run through**. For example, the intial values of P and Q of a specific load/feed could be called up here. The *control_step* describes the **actual control behavior of the controller**. *repair_control* enables you to make a **one-off correction for each control loop if the grid does not converge** due to the controller. For example, the handling of occurring NaN values could be described here. *finalize_control* is **executed at the end**. A typical example here is to set the converged flag back to False so that the controller is not inadvertently ingorized in the subsequent control loop. # # Other relevant functions are: # - is_converged # - set_recycle # # *is_converged* describes the conditions under which the **controller has stabilized**, i.e. has converged. *set_recycle* defines the extent to which the **Jacobian matrix may be reused** in successive controller loops. # # Furhter important functions are *time_step*, *restore_init_state*, *finalize_step*. However, these are only relevant for the **time series simulation** and for this reason we refer to the time series simulation tutorial at this point. # # These are the primarily relevant functions of any controller. If you want to build a controller yourself, you must always consider these functions. # ## Using the control module # This example, which is intended to familiarize you with the control module, uses the tap controller (TapControl). To do this, you must first load the MV-Oberrhein grid, which contains two 110/220 kV transformers: # In[7]: import pandapower as pp from pandapower.networks import mv_oberrhein net = mv_oberrhein() net.trafo # Here you can now see both transformers with all their properties. # # Next, you should calculate a power flow to see which voltage prevails on the upper and lower sides of the transformer. You can do this using the pp.runpp(net) command. Immediately afterwards, display the results: Once on the upper voltage side (not very exciting, as the slack hangs here) ... # In[8]: pp.runpp(net) net.res_trafo.vm_hv_pu # ... and once on the undervoltage side: # In[9]: net.res_trafo.vm_lv_pu # What you can also see on the net.trafo output: Both transformers contain a tap changer that is in the neutral position at level 0 and can be changed by 9 levels down and up. # In our case, both tap changers are in the following positions: # In[10]: net.trafo['tap_pos'] # The tap changer position does not change within a power flow. However, it is possible to use the tap controllers to influence the result of the powerflow on the upstream or downstream side depending on the node voltage. # ### Discrete Tap Control # # The discrete tap controller (DiscreteTapControl) in pandapower is first given the id of the transformer that is to be controlled. The default setting is that the voltage is checked on the undervoltage side. In addition, a deadband is passed in which the voltage at a particular node may move. In our case, we define a deadband of 0.99 and 1.01 p.u. for the first transformer in the Oberrhein grid: # In[11]: import pandapower.control as control trafo_controller = control.DiscreteTapControl(net=net, tid=114, vm_lower_pu=0.99, vm_upper_pu=1.00) # As mentioned above, this controller is also automatically registered in the network: # In[12]: net.controller # To activate the controllers, *run_control = True* must now be set when calling pp.runpp. A look at the results on the transformer on the low-voltage side reveals the extent to which the controllers have become active: # In[13]: pp.runpp(net, run_control=True) net.res_trafo.vm_lv_pu # As you can see, the voltage has decreased and is now within the specified range. If we also check the position of the switch position, we can see that the position has changed from -2 to -1 # In[14]: net.trafo['tap_pos'] # ### Continuous Tap Control # In addition to the discrete tap controller, there is also the option of using a continuous tap controller. The special feature here is that you do not have to specify a voltage range, but that an exact voltage to be achieved is specified (a tolerance range 'tol' only indicates when the result is accurate enough and no further control loop is required). In concrete terms, this means that it is assumed that the stages of a transformer do not have to be integer. In our example, you can use this controller for the second transformer, for example. # In[15]: trafo_controller = control.ContinuousTapControl(net=net, tid=142, vm_set_pu=0.98, tol=1e-6) # If you now carry out a power flow with activated controllers, the voltage on the undervoltage side is exactly 0.98 p.u.: # In[16]: pp.runpp(net, run_control=True) net.res_trafo.vm_lv_pu # Furthermore, as was to be expected, the tap changer position is no longer an integer, but is around -0.07: # In[17]: net.trafo['tap_pos'] # Even if this result cannot occur in reality, it can still be useful, for example to avoid large jumps in results in large-scale studies. # # In a short digression, the effect of level and order shall be displayed. If a another continuous trafo contoller in the same level is defined setting vm_set_pu = 0.99 as a result the controllers would not converge: # In[18]: trafo_controller = control.ContinuousTapControl(net=net, tid=142, vm_set_pu=0.99, tol=1e-6) pp.runpp(net, run_control=True) # However, if the level is increased in case of one of the two tap controller, the system would converge again, as the two controllers would not affect each other. The controller with the higher level would then be the superior controller as its set value would be the leading one. # In[19]: net.controller.at[2, 'level'] = 1 pp.runpp(net, run_control=True) net.res_trafo.vm_lv_pu # ## Lessons learned # # After completing this tutorial: # - you will have understood the structure of the control module. # - know what the central components of each controller are. # - know the difference between level and order. # - be able to embed a simple level controller in a grid. # In[ ]: