CURENT / andes

Python toolbox / library for power system transient dynamics simulation with symbolic modeling and numerical analysis 🔥
https://ltb.curent.org
Other
236 stars 112 forks source link

Offline bus #566

Open jinningwang opened 1 month ago

jinningwang commented 1 month ago

Describe the bug

When some Buses are offline (u=0), they seem to be still in power flow calculation.

To Reproduce

In this case, Bus with idx=1 is connected with Lines with idx in [0, 3, 6]

Minimal code:

ss = andes.load(andes.get_case('5bus/pjm5bus.xlsx'))
ss.Bus.set(src='u', attr='v', idx=1, value=0)
ss.PFlow.run()
ss.Line.a1.e

Return:

array([ 1.49607857,  1.59622949, -2.48838663, -0.02059051,  0.21368975,
       -2.15862025,  1.49607857])

Expected:

array([0,  1.59622949, -2.48838663, 0,  0.21368975,
       -2.15862025,  0])

Note: the involved line should have no line flow, and other places numerical values are not necessary to be accurate

Expected behavior

For Lines, their effective online status should take that into account that the connected bus status.

Desktop (please complete the following information):

**pip packages (please paste the output from pip list)

Additional context

Add any other context about the problem here.

jinningwang commented 1 month ago

Issue unexpected behaviors of find_idx()

For single input values, only return first match even there are multiple matches

Minimal code:

ss = andes.load(andes.get_case('ieee14/ieee14_pvd1.xlsx'))
ss.PVD1.find_idx(keys='bus', values=[4])

Return:

[1]

Expected return:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

For multiple inputs values, only return first match of first value

Minimal code:

ss = andes.load(andes.get_case('ieee14/ieee14_pvd1.xlsx'))
ss.DG.find_idx(keys='name', values=['PVD1_3', 'PVD1_4'])

Return:

[3]

Expected return:

[3, 4]

Proposed solution

Option1: clarify logic of find_idx()

If I misunderstood the intentional use, we could clarify it in docstring

Option2: improve code logic:

If I didn't misunderstand, I suggest following development:

Add param find_all=True to return all matches

Default as False to ensure consistent default behavior

Check all items in values

Improve group.find_idx() to follow it

cuihantao commented 1 month ago

idx is meant to be unique. Otherwise, it's an inconsistency in input data.

Not sure what went off, but with my fresh installation of ANDES, second case seems to work:

Python 3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:37:07) [Clang 15.0.7 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import andes
>>> ss = andes.load(andes.get_case('ieee14/ieee14_pvd1.xlsx'))
>>> ss.DG.find_idx(keys='name', values=['PVD1_3', 'PVD1_4'])
[3, 4]
cuihantao commented 1 month ago

The following is my result. It needs a fix.

>>> ss.PVD1.find_idx(keys='bus', values=[4])
[1]
jinningwang commented 1 month ago

idx is meant to be unique. Otherwise, it's an inconsistency in input data.

Not sure what went off, but with my fresh installation of ANDES, second case seems to work:

Python 3.11.6 | packaged by conda-forge | (main, Oct  3 2023, 10:37:07) [Clang 15.0.7 ] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import andes
>>> ss = andes.load(andes.get_case('ieee14/ieee14_pvd1.xlsx'))
>>> ss.DG.find_idx(keys='name', values=['PVD1_3', 'PVD1_4'])
[3, 4]

I doubled checked and you are right, this one work as expected. My end messed up due to my local development.

jinningwang commented 1 month ago

The following is my result. It needs a fix.

>>> ss.PVD1.find_idx(keys='bus', values=[4])
[1]

I'm happy to do it, and I am actually doing it. Welcome your further suggestions if anything worth our attention.

cuihantao commented 1 month ago

https://github.com/CURENT/andes/blob/5ab784b75347a98d07c004e32d7c4b39d407ea2e/andes/core/model/modeldata.py#L322-L334

It can be fixed from here. It might introduces one change. The output shall always be a list, which has the same length as values. Each element in the output list shall be a list, corresponding to each value input.

jinningwang commented 1 month ago

Develop a model ConnMan to handle connectivity check

To handle topology changes caused by turn off Bus and resulted other models' disconnection. This model can go to group Collection

In following conditions, it will trigger:

  1. Bus.u != 1; Or
  2. Bus.u is set/altered

Implementation:

  1. In System.setup() add a check
  2. Overwrite Bus.set() and Bus.alter() to trigger

Additional considerations:

  1. It seems this model should be a system level function. Developing it as a model is not the easiest way to cover the possible usage?

Pseudo code:

class ConnMan(ModelData, Model):
   """
   Connectivity Manager.
   """

    def __init__(self, system, config):
        ModelData.__init__(self)
        Model.__init__(self, system, config)

    def _act(self):
        """
        Update the connectivity.
        """
        if True:
            self._disconnect()
        else:
            self._connect()
        self.system.connectivity(info=True)
        return None

    def _disconnect(self):
        """
        Disconnect involved devices.
        """
        pass

    def _connect(self):
        """
        Connect involved devices.
        """
        pass
jinningwang commented 1 month ago

@cuihantao Hantao, my ongoing work of connectivity manager is in https://github.com/jinningwang/andes/blob/conm/andes/core/connman.py

The outline is excerpted below for discussion, your suggestions are very welcome.

class ConnMan:
    """
    Define a Connectivity Manager class for System
    """

    def __init__(self, system=None):
        """
        Initialize the connectivity manager.

        system: system object
        """
        self.system = system
        self.busu0 = None           # placeholder for Bus.u.v
        self.is_act = False         # flag for act, True to act

    def init(self):
        """
        Initialize the connectivity.
        """
        self.busu0 = self.system.Bus.u.v.copy()
        return None

    def record(self):
        """
        Record the bus connectivity in-place.
        """
        self.busu0[...] = self.system.Bus.u
        return None

    def act(self):
        """
        Update the connectivity.
        """
        if not self.is_act:
            logger.debug('Connectivity is not need to be updated.')
            return None

        # --- action ---
        pass

        self.system.connectivity(info=True)
        return None

My considerations:

  1. status flag is_act to enable lazy evaluation
  2. In model Bus, overwriting set and alter to set is_act if values changed
  3. In action part, use Model.set to achieve target devices connectivity change
  4. In this module, define an OrderedDict to define the connectivity check logic
cuihantao commented 1 month ago

Thanks! I know you are looking to implement a mechanism to handle offline devices by turning off connected devices. Its better to start the design with how you expect to to use it: when, where and how it’s going to be called. Sent from my iPhoneOn Oct 5, 2024, at 2:45 PM, Jinning Wang @.***> wrote: @cuihantao Hantao, my ongoing work of connectivity manager is in https://github.com/jinningwang/andes/blob/conm/andes/core/connman.py The outline is excerpted below for discussion, your suggestions are very welcome. class ConnMan: """ Define a Connectivity Manager class for System """

def __init__(self, system=None):
    """
    Initialize the connectivity manager.

    system: system object
    """
    self.system = system
    self.busu0 = None           # placeholder for Bus.u.v
    self.is_act = False         # flag for act, True to act

def init(self):
    """
    Initialize the connectivity.
    """
    self.busu0 = self.system.Bus.u.v.copy()
    return None

def record(self):
    """
    Record the bus connectivity in-place.
    """
    self.busu0[...] = self.system.Bus.u
    return None

def act(self):
    """
    Update the connectivity.
    """
    if not self.is_act:
        logger.debug('Connectivity is not need to be updated.')
        return None

    # --- action ---
    pass

    self.system.connectivity(info=True)
    return None

My considerations:

status flag is_act to enable lazy evaluation In model Bus, overwriting set and alter to set is_act if values changed In action part, use Model.set to achieve target devices connectivity change In this module, define an OrderedDict to define the connectivity check logic

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>

jinningwang commented 1 month ago

@cuihantao I summarized my thoughts as below.

Usage Scenarios

  Description Results now Results expected
S1 After loading a case, there are offline buses. The offline buses and lines connected to them are still effective in PFlow and TDS. The offline buses and lines connected to them should be out of use.
S2 Before PFlow or TDS init, change bus online status. Same as above Same as above
S3 After the initialization/run of PFlow, change bus online status. Same as above PFlow should be reset.
S4 After the initialization/run of TDS, change bus online status is not allowed Nothing happened. Does not allow such operation as we don’t model the transient process of turning off a bus.

Proposed development efforts

Develop a module named ConnMan, and include it as a system level function. It means a System will have the module as an attribute System.conn

When described things happened, mark a flag.

Before running any analysis routine, do the connectivity action if flagged.

cuihantao commented 1 month ago

Can you turn these descriptions into code which describes how you expect to use your design? That will also help you think about the design.

The class API design is not the usage code.

jinningwang commented 1 month ago

Implementation plan

Connectivity Manager Class

class System has an attribute conn which is an instance of class ConnMan.

>>> ss = andes.load(andes.get_case('ieee14/ieee14_conn.xlsx'))
>>> ss.conn
<andes.core.connman.ConnMan at 0x11cb3f2b0>
>>> ss.conn.busu0
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

Usage Scenarios

Action flag triggering condition

Let say, there is one bus offline in any following way:

  1. when loading case, there is any bus offline
  2. when running any routine, bus online status is not the same with last stored and connectivity passed online status

The connectivity action flag should be True.

>>> ss.Bus.u.v
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]

>>> ss.conn.to_act
True

Before a routine is initialized

When running any routine analysis, the system will check the connectivity action flag, and run the connectivity act if it is True.

This is applicable for all the three routiens, PFlow, TDS, and EIG.

# In the `Routine.init()`, add a flag check
class Routinebase:

    def init(self):
        if self.system.conn.to_act:
            self.system.conn.act()
        ...  # existing code

The logging message should be like:

>>> ss.<Routine>.init()
Connectivity action is required. Running connectivity action...
Bus <15> is offline, involved Line <15-16> is turned off.

After a routine is initialized

For PFlow and EIG, once the action flag is true, the routine results should be reset.

For TDS, once the action flag is true, the Bus online status should not be changed.

>>> ... # previous code that a Bus is offline
>>> ss.PFlow.init()
Connectivity action is required. Running connectivity action...
Bus <15> is offline, involved Line <15-16> is turned off.

>>> ... # previous code that a Bus is offline
>>> ss.EIG.init()
Connectivity action is required. Running connectivity action...
Bus <15> is offline, involved Line <15-16> is turned off.

>>> ... # previous code that a Bus is offline
>>> ss.TDS.init()
Bus connectivity status changed, but it is not allowed after TDS initialization.
The action is not taken, and TDS running is aborted.