diff --git a/changelog.md b/changelog.md index 53a1ebe8..591ffbd7 100644 --- a/changelog.md +++ b/changelog.md @@ -10,7 +10,7 @@ * None. ### Fixes -* None. +* Reordered the feeder equipment and direction assignment on database read to prevent parallel feeders from tracing back into the zone substation. ### Notes * None. diff --git a/src/zepben/ewb/database/sqlite/network/network_database_reader.py b/src/zepben/ewb/database/sqlite/network/network_database_reader.py index 4c009c6a..54b94d26 100644 --- a/src/zepben/ewb/database/sqlite/network/network_database_reader.py +++ b/src/zepben/ewb/database/sqlite/network/network_database_reader.py @@ -75,6 +75,21 @@ def __init__( async def _post_load(self) -> bool: status = await super()._post_load() + # + # NOTE: We need to have the feeder head equipment assigned before we can set the feeder directions to prevent + # tracing back into the zone substation in parallel feeders. Rather than splitting the feeder assignment + # into two passes, we can just assign the equipment to feeders before we set the directions. + # + self._logger.info("Assigning equipment to feeders...") + await self.assign_to_feeders.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) + await self.assign_to_feeders.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) + self._logger.info("Equipment assigned to feeders.") + + self._logger.info("Assigning equipment to LV feeders...") + await self.assign_to_lv_feeders.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) + await self.assign_to_lv_feeders.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) + self._logger.info("Equipment assigned to LV feeders.") + self._logger.info("Applying feeder direction to network...") await self.set_feeder_direction.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) await self.set_feeder_direction.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) @@ -91,16 +106,6 @@ async def _post_load(self) -> bool: self._logger.info("Phasing applied to network.") - self._logger.info("Assigning equipment to feeders...") - await self.assign_to_feeders.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) - await self.assign_to_feeders.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) - self._logger.info("Equipment assigned to feeders.") - - self._logger.info("Assigning equipment to LV feeders...") - await self.assign_to_lv_feeders.run(self.service, network_state_operators=NetworkStateOperators.NORMAL) - await self.assign_to_lv_feeders.run(self.service, network_state_operators=NetworkStateOperators.CURRENT) - self._logger.info("Equipment assigned to LV feeders.") - self._logger.info("Validating that each equipment is assigned to a container...") self._validate_equipment_containers() self._logger.info("Equipment containers validated.") diff --git a/src/zepben/ewb/services/network/tracing/feeder/set_direction.py b/src/zepben/ewb/services/network/tracing/feeder/set_direction.py index 56a75008..265b002f 100644 --- a/src/zepben/ewb/services/network/tracing/feeder/set_direction.py +++ b/src/zepben/ewb/services/network/tracing/feeder/set_direction.py @@ -33,6 +33,12 @@ class SetDirection: """ Convenience class that provides methods for setting feeder direction on a [NetworkService] This class is backed by a [BranchRecursiveTraversal]. + + NOTE: The feeder head equipment must be assigned to its [Feeder] before this is run. If you don't, + the feeder direction will be assigned back through feeder heads when they are run in parallel. + + :param debug_logger: An optional `Logger` that can be used to log debug messages on what the underlying trace is doing. This + should only ever be used in a debug cycle, and should always be `None` in production code. """ def __init__(self, debug_logger: Logger = None): diff --git a/test/database/sqlite/network/test_network_database_reader.py b/test/database/sqlite/network/test_network_database_reader.py index 5a7f7a54..9fab3bcd 100644 --- a/test/database/sqlite/network/test_network_database_reader.py +++ b/test/database/sqlite/network/test_network_database_reader.py @@ -111,11 +111,11 @@ async def test_calls_expected_processors_including_post_processes(self): call.service.unresolved_references(), call.service.unresolved_references().__iter__, + call.assign_to_feeders.run(self.service), + call.assign_to_lv_feeders.run(self.service), call.set_direction.run(self.service), call.set_phases.run(self.service), call.phase_inferrer.run(self.service), - call.assign_to_feeders.run(self.service), - call.assign_to_lv_feeders.run(self.service), # calls for _validate_equipment_containers() call.service.objects(Equipment), diff --git a/test/database/sqlite/network/test_network_database_schema.py b/test/database/sqlite/network/test_network_database_schema.py index f5410c76..88b9bd37 100644 --- a/test/database/sqlite/network/test_network_database_schema.py +++ b/test/database/sqlite/network/test_network_database_schema.py @@ -41,7 +41,7 @@ PotentialTransformer, SwitchInfo, RelayInfo, CurrentRelay, EvChargingUnit, TapChangerControl, DistanceRelay, VoltageRelay, ProtectionRelayScheme, \ ProtectionRelaySystem, Ground, GroundDisconnector, SeriesCompensator, NetworkService, GroundingImpedance, \ PetersenCoil, ReactiveCapabilityCurve, SynchronousMachine, PanDemandResponseFunction, BatteryControl, StaticVarCompensator, Tracing, NetworkStateOperators, \ - NetworkTraceStep + NetworkTraceStep, TestNetworkBuilder from zepben.ewb.model.cim.iec61968.assetinfo.cable_info import CableInfo from zepben.ewb.model.cim.iec61968.assetinfo.overhead_wire_info import OverheadWireInfo from zepben.ewb.model.cim.iec61968.assets.asset_owner import AssetOwner @@ -66,13 +66,13 @@ from zepben.ewb.model.cim.iec61970.base.wires.ratio_tap_changer import RatioTapChanger from zepben.ewb.services.common import resolver - hypothesis_settings = dict( deadline=2000, suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.too_slow], max_examples=4 ) + # FIXME: see Line [305] class PatchedNetworkTraceStepPath(NetworkTraceStep.Path): @property @@ -702,6 +702,24 @@ def add_deferred_reference(service: NetworkService): await self._validate_unresolved_failure(str(pec), "RegulatingControl tcc", add_deferred_reference) + async def test_assigns_feeders_in_parallel_correctly(self): + # This test is to ensure parallel feeders don't assign directions back through the feeder heads. This was seen in the wild when + # the feeder directions were set before the equipment was assigned, meaning no feeder heads were detected in the tracing. + ns = await ( + TestNetworkBuilder() + .from_source() # s0 + .to_breaker() # b1 + .to_acls() # c2 + .to_breaker() # b3 + .to_source() # s4 + .add_feeder("b1", 2) + .add_feeder("b3", 1) + .build(apply_directions_from_sources=False) + ) + + # If the read from the database matches the test network we built, then the equipment is correctly assigned. + await self._validate_schema(ns) + async def test_only_loads_street_address_fields_if_required(self): # This test is here to make sure the database reading correctly removes the parts of loaded street addresses that are not filled out. write_service = NetworkService()