From c7840f921829569ad3b5c1d655c19aaac0a7e60d Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Fri, 16 May 2025 14:35:27 +1000 Subject: [PATCH 01/12] Updating examples to use APIv2 untested as yet --- src/zepben/examples/tracing.py | 65 ++++++++++--------- .../tracing_conductor_type_by_lv_circuit.py | 24 +++---- src/zepben/examples/tracing_example.py | 36 +++++----- .../translating_to_pandapower_model.py | 4 +- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/zepben/examples/tracing.py b/src/zepben/examples/tracing.py index bc50967..2ee1d1b 100644 --- a/src/zepben/examples/tracing.py +++ b/src/zepben/examples/tracing.py @@ -8,15 +8,10 @@ # The Evolve SDK contains several factory functions for traversals that cover common use cases. import asyncio -from zepben.evolve import Switch, connected_equipment_trace, ConductingEquipmentStep, connected_equipment_breadth_trace, \ - normal_connected_equipment_trace, current_connected_equipment_trace, connectivity_trace, ConnectivityResult, connected_equipment, \ - connectivity_breadth_trace, SinglePhaseKind, normal_connectivity_trace, current_connectivity_trace, phase_trace, PhaseCode, PhaseStep, normal_phase_trace, \ - current_phase_trace, assign_equipment_to_feeders, Feeder, LvFeeder, assign_equipment_to_lv_feeders, set_direction, Terminal, \ - normal_limited_connected_equipment_trace, AcLineSegment, current_limited_connected_equipment_trace, FeederDirection, remove_direction, \ - normal_downstream_trace, current_downstream_trace, TreeNode, Breaker - -from zepben.evolve.services.network.tracing.phases import phase_step -from zepben.evolve.services.network.tracing.tracing import normal_upstream_trace, current_upstream_trace, normal_downstream_tree, current_downstream_tree +from zepben.evolve import Switch, ConnectivityResult, connected_equipment, SinglePhaseKind, PhaseCode, \ + Feeder, LvFeeder, Terminal, AcLineSegment, FeederDirection, Breaker, Tracing, NetworkStateOperators, NetworkTraceStep, StepContext +from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import EquipmentTreeBuilder +from zepben.evolve.services.network.tracing.networktrace.actions.tree_node import TreeNode # For the purposes of this example, we will use the IEEE 13 node feeder. from zepben.examples.ieee_13_node_test_feeder import network @@ -26,6 +21,9 @@ hv_feeder = network.get("hv_fdr", Feeder) lv_feeder = network.get("lv_fdr", LvFeeder) +NORMAL = NetworkStateOperators.NORMAL +CURRENT = NetworkStateOperators.CURRENT + def reset_switch(): switch.set_normally_open(False) @@ -46,17 +44,18 @@ async def equipment_traces(): print_heading("EQUIPMENT TRACING") # noinspection PyArgumentList - start_item = ConductingEquipmentStep(conducting_equipment=feeder_head) + start_item = feeder_head visited = set() - async def print_step(ces: ConductingEquipmentStep, _): - visited.add(ces.conducting_equipment) - print(f"\tDepth {ces.step:02d}: {ces.conducting_equipment}") + async def print_step(ces: NetworkTraceStep, ctx: StepContext): + visited.add(ces.path.to_equipment) + print(f"\tDepth {ctx.step_number:02d}: {ces.path.to_equipment}") # The connected equipment trace iterates through all connected equipment depth-first, and even through open switches. # Equipment will be revisited if a shorter path from the starting equipment is found. print("Connected Equipment Trace:") - await connected_equipment_trace().add_step_action(print_step).run(start_item) + await Tracing.network_trace(network_state_operators=NORMAL).add_step_action(print_step).run(start_item) + await Tracing.network_trace(network_state_operators=CURRENT).add_step_action(print_step).run(start_item) print(f"Number of equipment visited: {len(visited)}") print() visited.clear() @@ -225,7 +224,7 @@ async def print_phase_step(step: PhaseStep, _: bool): # set_direction() must be run on a network before running directed traces. # Note that set_direction() does not trace through switches with at least one open phase, # meaning that terminals beyond such a switch are left with a feeder direction of NONE. - await set_direction().run(network) + await Tracing.set_direction().run(network) print(f"Feeder direction set for each terminal.") print() @@ -253,7 +252,7 @@ async def print_phase_step(step: PhaseStep, _: bool): print() visited.clear() - remove_direction().run(network) + Tracing.clear_direction().run(network) print(f"Feeder direction removed for each terminal.") print() @@ -269,8 +268,8 @@ async def assigning_equipment_to_feeders(): print(f"LV feeders powered by HV feeder: {[lvf.mrid for lvf in hv_feeder.normal_energized_lv_feeders]}") print(f"HV feeders powering LV feeder: {[hvf.mrid for hvf in lv_feeder.normal_energizing_feeders]}") print() - await assign_equipment_to_feeders().run(network) - await assign_equipment_to_lv_feeders().run(network) + await Tracing.assign_equipment_to_feeders().run(network) + await Tracing.assign_equipment_to_lv_feeders().run(network) print("Equipment assigned to feeders.") print() print(f"Equipment in HV feeder: {[eq.mrid for eq in hv_feeder.equipment]}") @@ -281,7 +280,7 @@ async def assigning_equipment_to_feeders(): async def set_and_remove_feeder_direction(): - # Use set_direction().run(network) to evaluate the feeder direction of each terminal. + # Use Tracing.set_direction().run(network) to evaluate the feeder direction of each terminal. print_heading("SETTING FEEDER DIRECTION") switch.set_normally_open(True, phase=SinglePhaseKind.A) print(f"Switch set to normally open on phase A. Switch is between feeder head and energy consumer 675.") @@ -292,7 +291,8 @@ async def set_and_remove_feeder_direction(): print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") print() - await set_direction().run(network) + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.CURRENT) print("Normal and current feeder direction set.") print() print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") @@ -301,8 +301,8 @@ async def set_and_remove_feeder_direction(): print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") print() - # Use remove_direction().run(network) to remove feeder directions. - # While set_direction().run(network) must be awaited, remove_direction().run(network) does not, because it is not asynchronous. + # Use Tracing.clear_direction().run(network) to remove feeder directions. + # While Tracing.set_direction().run(network) must be awaited, remove_direction().run(network) does not, because it is not asynchronous. print_heading("REMOVING FEEDER DIRECTION") consumer_terminal = network.get("ec_675_t", Terminal) @@ -311,7 +311,8 @@ async def set_and_remove_feeder_direction(): print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") print() - remove_direction().run(network) + Tracing.clear_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) + Tracing.clear_direction().run(network, network_state_operators=NetworkStateOperators.CURRENT) print("Normal and current feeder direction removed.") print() print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") @@ -340,7 +341,7 @@ def desc_lines(node: TreeNode): yield f"{stem_char} {line}" def print_tree(root_node: TreeNode): - print(root_node.conducting_equipment) + print(root_node.identified_object) for line in desc_lines(root_node): print(line) @@ -348,21 +349,25 @@ def print_tree(root_node: TreeNode): print("Switch set to currently open on phase C.") print() - await set_direction().run(network) + await Tracing.set_direction().run(network) print("Feeder direction set.") print() print("Normal Downstream Tree:") - ndt = await normal_downstream_tree().run(feeder_head) - print_tree(ndt) + equip_tree_builder = EquipmentTreeBuilder() + await Tracing.network_trace().add_step_action(equip_tree_builder).run(feeder_head) + print_tree(next(equip_tree_builder.roots)) print() print("Current Downstream Tree:") - cdt = await current_downstream_tree().run(feeder_head) - print_tree(cdt) + cur_equip_tree_builder = EquipmentTreeBuilder() + await Tracing.network_trace( + network_state_operators=NetworkStateOperators.CURRENT + ).add_step_action(cur_equip_tree_builder).run(feeder_head) + print_tree(next(cur_equip_tree_builder.roots)) print() - remove_direction().run(network) + Tracing.clear_direction().run(network) print(f"Feeder direction removed for each terminal.") print() diff --git a/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py b/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py index 30ffd06..5457a10 100644 --- a/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py +++ b/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py @@ -37,12 +37,12 @@ async def main(): print(f"Failed to retrieve feeder {feeder.mrid}") continue for io in network.objects(Switch): - loop = False + _loop = False for t in io.terminals: t_dir = t.normal_feeder_direction if t_dir == FeederDirection.BOTH: - loop = True + _loop = True sw_name = io.name sw_id = io.mrid @@ -81,24 +81,26 @@ async def get_feeder_network(channel, feeder_mrid): return client.service -async def get_downstream_trace(ce: PhaseStep) -> list[Any]: - trace = normal_downstream_trace() +async def get_downstream_trace(ce: ConductingEquipment, phase_code: PhaseCode) -> list[Any]: + state_operators = NetworkStateOperators.NORMAL + trace = Tracing.network_trace().add_condition(state_operators.downstream()) l_type: [str, str, float] = [] def collect_eq_in(): - async def add_eq(ps, _): - if isinstance(ps.conducting_equipment, AcLineSegment): - l_type.append(ps.conducting_equipment.mrid) - l_type.append(ps.conducting_equipment.asset_info.name) - if ps.conducting_equipment.length is not None: - l_type.append(ps.conducting_equipment.length) + async def add_eq(ps: NetworkTraceStep, _): + equip = ps.path.to_equipment + if isinstance(equip, AcLineSegment): + l_type.append(equip.mrid) + l_type.append(equip.asset_info.name) + if equip.length is not None: + l_type.append(equip.length) else: l_type.append(0) return add_eq trace.add_step_action(collect_eq_in()) - await trace.run(ce) + await trace.run(start=ce, phases=phase_code) return l_type diff --git a/src/zepben/examples/tracing_example.py b/src/zepben/examples/tracing_example.py index 0bb9758..3663533 100644 --- a/src/zepben/examples/tracing_example.py +++ b/src/zepben/examples/tracing_example.py @@ -8,9 +8,8 @@ import asyncio import json -from zepben.evolve import NetworkConsumerClient, PhaseStep, PhaseCode, AcLineSegment, normal_downstream_trace, connect_with_token, EnergyConsumer, \ - PowerTransformer, normal_upstream_trace -from zepben.evolve.services.network.tracing.phases.phase_step import start_at +from zepben.evolve import NetworkConsumerClient, PhaseCode, AcLineSegment, connect_with_token, EnergyConsumer, \ + PowerTransformer, ConductingEquipment, Tracing, NetworkStateOperators, NetworkTraceStep from zepben.protobuf.nc.nc_requests_pb2 import IncludedEnergizedContainers with open("config.json") as f: @@ -36,13 +35,13 @@ async def main(): print("Downstream Trace Example..") # Get the count of customers per transformer for io in network.objects(PowerTransformer): - customers = await get_downstream_customer_count(start_at(io, PhaseCode.ABCN)) + customers = await get_downstream_customer_count(io, PhaseCode.ABCN) print(f"Transformer {io.mrid} has {customers} Energy Consumer(s)") print() print("Upstream Trace Example..") for ec in network.objects(EnergyConsumer): - upstream_length = await get_upstream_length(start_at(ec, PhaseCode.ABCN)) + upstream_length = await get_upstream_length(ec, PhaseCode.ABCN) print(f"Energy Consumer {ec.mrid} --> Upstream Length: {upstream_length}") @@ -53,36 +52,39 @@ async def get_feeder_network(channel, feeder_mrid): return client.service -async def get_downstream_customer_count(ce: PhaseStep) -> int: - trace = normal_downstream_trace() +async def get_downstream_customer_count(ce: ConductingEquipment, phase_code: PhaseCode) -> int: + state_operators = NetworkStateOperators.NORMAL + trace = Tracing.network_trace().add_condition(state_operators.downstream()) customer_count = 0 def collect_eq_in(): - async def add_eq(ps, _): + async def add_eq(ps: NetworkTraceStep, _): nonlocal customer_count - if isinstance(ps.conducting_equipment, EnergyConsumer): + if isinstance(ps.path.to_equipment, EnergyConsumer): customer_count += 1 return add_eq trace.add_step_action(collect_eq_in()) - await trace.run(ce) + await trace.run(start=ce, phases=phase_code) return customer_count -async def get_upstream_length(ce: PhaseStep) -> int: - trace = normal_upstream_trace() +async def get_upstream_length(ce: ConductingEquipment, phases: PhaseCode) -> int: + state_operators = NetworkStateOperators.NORMAL + trace = Tracing.network_trace().add_condition(state_operators.upstream()) upstream_length = 0 def collect_eq_in(): - async def add_eq(ps, _): + async def add_eq(ps: NetworkTraceStep, _): nonlocal upstream_length - if isinstance(ps.conducting_equipment, AcLineSegment): - if ps.conducting_equipment.length is not None: - upstream_length = upstream_length + ps.conducting_equipment.length + equip = ps.path.to_equipment + if isinstance(equip, AcLineSegment): + if equip.length is not None: + upstream_length += equip.length return add_eq trace.add_step_action(collect_eq_in()) - await trace.run(ce) + await trace.run(start=ce, phases=phases) return upstream_length if __name__ == "__main__": diff --git a/src/zepben/examples/translating_to_pandapower_model.py b/src/zepben/examples/translating_to_pandapower_model.py index 771e2cc..de8ea0e 100644 --- a/src/zepben/examples/translating_to_pandapower_model.py +++ b/src/zepben/examples/translating_to_pandapower_model.py @@ -8,7 +8,7 @@ import pandapower as pp from pp_creators.basic_creator import BasicPandaPowerNetworkCreator -from zepben.evolve import set_direction, NetworkService, Terminal, EnergySource +from zepben.evolve import NetworkService, Terminal, EnergySource, Tracing from zepben.examples.ieee_13_node_test_feeder import network @@ -17,7 +17,7 @@ async def main(): add_energy_source(network, network["br_650_t1"]) - await set_direction().run(network) + await Tracing.set_direction().run(network) bbn_creator = BasicPandaPowerNetworkCreator( logger=logger, ec_load_provider=lambda ec: (5000, 0) # Model each energy consumer with a 5kW nonreactive load From 446e4e490de6f5b8473181bf746b611afa6c6a77 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 13:59:03 +1000 Subject: [PATCH 02/12] unnecessary reformat of a file because not using if __name... annoyed me Signed-off-by: Max Chesterfield --- .../examples/building_network_hierarchy.py | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/src/zepben/examples/building_network_hierarchy.py b/src/zepben/examples/building_network_hierarchy.py index 383ef0c..135eeba 100644 --- a/src/zepben/examples/building_network_hierarchy.py +++ b/src/zepben/examples/building_network_hierarchy.py @@ -1,4 +1,4 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -6,41 +6,43 @@ from zepben.evolve import NetworkHierarchy, GeographicalRegion, SubGeographicalRegion, Feeder, Substation, Loop, Circuit -# A network hierarchy describes the high-level hierarchy of the network. - -fdr1 = Feeder(name="Sydney feeder 1") -fdr2 = Feeder(name="Sydney feeder 2") -fdr3 = Feeder(name="Sydney feeder 3") -fdr4 = Feeder(name="Newcastle feeder 1") -fdr5 = Feeder(name="Newcastle feeder 2") -fdr6 = Feeder(name="Newcastle feeder 3") - -sub1 = Substation(name="Sydney substation 1") -sub2 = Substation(name="Sydney substation 2", normal_energized_feeders=[fdr1, fdr2, fdr3]) -sub3 = Substation(name="Newcastle substation", normal_energized_feeders=[fdr4, fdr5, fdr6]) - -circuit_sydney = Circuit(end_substations=[sub1, sub2]) -loop_sydney = Loop(circuits=[circuit_sydney], substations=[sub1], energizing_substations=[sub2]) -sgr_sydney = SubGeographicalRegion(name="Sydney", substations=[sub1, sub2]) -sgr_newcastle = SubGeographicalRegion(name="Newcastle", substations=[sub3]) - -gr_nsw = GeographicalRegion(name="New South Wales", sub_geographical_regions=[sgr_sydney, sgr_newcastle]) - -network_hierarchy = NetworkHierarchy( - geographical_regions={gr_nsw.mrid: gr_nsw}, - sub_geographical_regions={sgr.mrid: sgr for sgr in (sgr_sydney, sgr_newcastle)}, - substations={sub.mrid for sub in (sub1, sub2, sub3)}, - feeders={fdr.mrid: fdr for fdr in (fdr1, fdr2, fdr3, fdr4, fdr5, fdr6)}, - circuits={circuit_sydney.mrid: circuit_sydney}, - loops={loop_sydney.mrid: loop_sydney} -) - -print("Network hierarchy:") -for gr in network_hierarchy.geographical_regions.values(): - print(f"- {gr.name}") - for sgr in gr.sub_geographical_regions: - print(f" - {sgr.name}") - for sub in sgr.substations: - print(f" - {sub.name}") - for fdr in sub.feeders: - print(f" - {fdr.name}") \ No newline at end of file +if __name__ == '__main__': + + # A network hierarchy describes the high-level hierarchy of the network. + + fdr1 = Feeder(name="Sydney feeder 1") + fdr2 = Feeder(name="Sydney feeder 2") + fdr3 = Feeder(name="Sydney feeder 3") + fdr4 = Feeder(name="Newcastle feeder 1") + fdr5 = Feeder(name="Newcastle feeder 2") + fdr6 = Feeder(name="Newcastle feeder 3") + + sub1 = Substation(name="Sydney substation 1") + sub2 = Substation(name="Sydney substation 2", normal_energized_feeders=[fdr1, fdr2, fdr3]) + sub3 = Substation(name="Newcastle substation", normal_energized_feeders=[fdr4, fdr5, fdr6]) + + circuit_sydney = Circuit(end_substations=[sub1, sub2]) + loop_sydney = Loop(circuits=[circuit_sydney], substations=[sub1], energizing_substations=[sub2]) + sgr_sydney = SubGeographicalRegion(name="Sydney", substations=[sub1, sub2]) + sgr_newcastle = SubGeographicalRegion(name="Newcastle", substations=[sub3]) + + gr_nsw = GeographicalRegion(name="New South Wales", sub_geographical_regions=[sgr_sydney, sgr_newcastle]) + + network_hierarchy = NetworkHierarchy( + geographical_regions={gr_nsw.mrid: gr_nsw}, + sub_geographical_regions={sgr.mrid: sgr for sgr in (sgr_sydney, sgr_newcastle)}, + substations={sub.mrid for sub in (sub1, sub2, sub3)}, + feeders={fdr.mrid: fdr for fdr in (fdr1, fdr2, fdr3, fdr4, fdr5, fdr6)}, + circuits={circuit_sydney.mrid: circuit_sydney}, + loops={loop_sydney.mrid: loop_sydney} + ) + + print("Network hierarchy:") + for gr in network_hierarchy.geographical_regions.values(): + print(f"- {gr.name}") + for sgr in gr.sub_geographical_regions: + print(f" - {sgr.name}") + for sub in sgr.substations: + print(f" - {sub.name}") + for fdr in sub.feeders: + print(f" - {fdr.name}") \ No newline at end of file From 397152f0e243d925416486e259915080f6a97419 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 13:59:58 +1000 Subject: [PATCH 03/12] moved import statements into function to more clearly demonstrate isolated examples. reformat of long lines, etc Signed-off-by: Max Chesterfield --- .../examples/connecting_to_grpc_service.py | 96 ++++++++++++++----- 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/src/zepben/examples/connecting_to_grpc_service.py b/src/zepben/examples/connecting_to_grpc_service.py index 18509dd..d6fdf3d 100644 --- a/src/zepben/examples/connecting_to_grpc_service.py +++ b/src/zepben/examples/connecting_to_grpc_service.py @@ -1,22 +1,16 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -import asyncio -import json - -from zepben.auth import AuthMethod -from zepben.evolve import connect_insecure, NetworkConsumerClient, connect_tls, connect_with_password, connect_with_secret, SyncNetworkConsumerClient, \ - connect_with_token - - -with open("config.json") as f: - c = json.loads(f.read()) async def plaintext_connection(): - """ Connects to an RPC server without TLS or authentication. This method should only be used in development and for demos. """ + """ + Connects to an RPC server without TLS or authentication. + This method should only be used in development and for demos. + """ + from zepben.evolve import connect_insecure, NetworkConsumerClient async with connect_insecure("hostname", 1234) as insecure_channel: client = NetworkConsumerClient(insecure_channel) grpc_result = await client.get_network_hierarchy() @@ -24,7 +18,11 @@ async def plaintext_connection(): async def secure_connection(): - """ Connects to an RPC server over TLS. No user/client credentials are used. """ + """ + Connects to an RPC server over TLS. + No user/client credentials are used. + """ + from zepben.evolve import connect_tls, NetworkConsumerClient async with connect_tls("hostname", 1234) as secure_channel: client = NetworkConsumerClient(secure_channel) grpc_result = await client.get_network_hierarchy() @@ -33,17 +31,34 @@ async def secure_connection(): async def secure_connection_with_user_credentials(): """ - Connects to an RPC server over TLS with user credentials. The authentication config will be fetched from - https://hostname/auth or https://hostname/ewb/auth by default, which includes the domain of the OAuth token provider. + Connects to an RPC server over TLS with user credentials. The authentication config will be + fetched from https://hostname/auth or https://hostname/ewb/auth by default, which includes + the domain of the OAuth token provider. """ - async with connect_with_password("client ID", "username", "password", "hostname", 1234) as secure_channel: + from zepben.evolve import connect_with_password, NetworkConsumerClient + async with connect_with_password( + "client ID", + "username", + "password", + "hostname", + 1234 + ) as secure_channel: client = NetworkConsumerClient(secure_channel) grpc_result = await client.get_network_hierarchy() print(grpc_result.result) # Specify authentication config explicitly - async with connect_with_password("client ID", "username", "password", "hostname", 1234, - audience="https://fake_audience/", issuer_domain="fake.issuer.domain", auth_method=AuthMethod.AUTH0) as secure_channel: + from zepben.auth import AuthMethod + async with connect_with_password( + "client ID", + "username", + "password", + "hostname", + 1234, + audience="https://fake_audience/", + issuer_domain="fake.issuer.domain", + auth_method=AuthMethod.AUTH0 + ) as secure_channel: client = NetworkConsumerClient(secure_channel) grpc_result = await client.get_network_hierarchy() print(grpc_result.result) @@ -51,25 +66,43 @@ async def secure_connection_with_user_credentials(): async def secure_connection_with_client_credentials(): """ - Connects to an RPC server over TLS with client credentials. The authentication config will be fetched from - https://hostname/auth or https://hostname/ewb/auth by default, which includes the domain of the OAuth token provider. + Connects to an RPC server over TLS with client credentials. The authentication config will be + fetched from https://hostname/auth or https://hostname/ewb/auth by default, which includes the + domain of the OAuth token provider. """ - async with connect_with_secret("client ID", "client secret", "hostname", 1234) as secure_channel: + from zepben.evolve import connect_with_secret, NetworkConsumerClient + async with connect_with_secret( + "client ID", + "client secret", + "hostname", + 1234 + ) as secure_channel: client = NetworkConsumerClient(secure_channel) grpc_result = await client.get_network_hierarchy() print(grpc_result.result) # Specify authentication config explicitly - async with connect_with_secret("client ID", "client secret", "hostname", 1234, - audience="https://fake_audience/", issuer_domain="fake.issuer.domain", auth_method=AuthMethod.AUTH0) as secure_channel: + from zepben.auth import AuthMethod + async with connect_with_secret( + "client ID", + "client secret", + "hostname", + 1234, + audience="https://fake_audience/", + issuer_domain="fake.issuer.domain", + auth_method=AuthMethod.AUTH0 + ) as secure_channel: client = NetworkConsumerClient(secure_channel) grpc_result = await client.get_network_hierarchy() print(grpc_result.result) -# You may use `SyncNetworkConsumerClient` if you prefer not to use asyncio. -# The API calls are the same between `SyncNetworkConsumerClient` and `NetworkConsumerClient`. def connect_sync(): + """ + You may use `SyncNetworkConsumerClient` if you prefer not to use asyncio. + The API calls are the same between `SyncNetworkConsumerClient` and `NetworkConsumerClient`. + """ + from zepben.evolve import connect_insecure, SyncNetworkConsumerClient channel = connect_insecure("hostname", 1234) client = SyncNetworkConsumerClient(channel) grpc_result = client.get_network_hierarchy() @@ -77,8 +110,18 @@ def connect_sync(): async def connect_using_token(): + import json + from zepben.evolve import connect_with_token, NetworkConsumerClient + + with open("config.json") as f: + c = json.loads(f.read()) + print("Connecting to EWB..") - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + channel = connect_with_token( + host=c["host"], + access_token=c["access_token"], + rpc_port=c["rpc_port"] + ) client = NetworkConsumerClient(channel) print("Connection established..") print("Printing network hierarchy..") @@ -87,4 +130,5 @@ async def connect_using_token(): if __name__ == "__main__": + import asyncio asyncio.run(connect_using_token()) From b40f9e947fc3b26a9d4042ee624aef376ac83ab1 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 15:32:32 +1000 Subject: [PATCH 04/12] updated to use new API, minor tweaks elsewhere Signed-off-by: Max Chesterfield --- .../examples/current_state_manipulations.py | 160 +++++++++--------- 1 file changed, 76 insertions(+), 84 deletions(-) diff --git a/src/zepben/examples/current_state_manipulations.py b/src/zepben/examples/current_state_manipulations.py index 5ce2759..689b140 100644 --- a/src/zepben/examples/current_state_manipulations.py +++ b/src/zepben/examples/current_state_manipulations.py @@ -8,46 +8,34 @@ import sys from typing import List, Set -from zepben.evolve import Feeder, PowerTransformer, Switch, assign_equipment_to_feeders, set_phases, NetworkConsumerClient, \ - connect_with_password, BusbarSection, tracing, ConductingEquipment, ConductingEquipmentStep, Terminal, Breaker, EquipmentContainer +from zepben.evolve import ( + Feeder, PowerTransformer, Switch, Tracing, NetworkConsumerClient, connect_with_password, Terminal, + BusbarSection, ConductingEquipment, Breaker, EquipmentContainer, StepContext, NetworkTraceStep +) + +from zepben.protobuf.nc.nc_requests_pb2 import IncludedEnergizedContainers, IncludedEnergizingContainers """ Primary question to answer/example for: -1. How to access the CIM model? Show examples of how the static/design and dynamic/current states of the network model is typically accessed by software - developers? +1. How to access the CIM model? Show examples of how the static/design and dynamic/current states + of the network model is typically accessed by software developers? a. Can Zepben model be updated with dynamic ADMS state information - b. if we have current state of network (dynamically updated from ADMS), can we query the model to find all current connected HV feeders in a voltage bus? - (VVC Example) - c. How fast can we retrieve a model (dynamic sate) from CIM for near real time applications? (VVC Example) + b. if we have current state of network (dynamically updated from ADMS), can we query the model + to find all current connected HV feeders in a voltage bus? (VVC Example) + c. How fast can we retrieve a model (dynamic sate) from CIM for near real time applications? + (VVC Example) 2. Show how the static and dynamic states of the network model is used by applications. """ async def run_simple(client: NetworkConsumerClient): - print() - print() - print("######################") - print("# FETCH ZONE FEEDERS #") - print("######################") - print() - print() - await fetch_zone_feeders(client) - print() - print() - print("##################") - print("# SPUR ISOLATION #") - print("##################") - print() - await isolate_spur_current(client) - print() - print() - print("##################") - print("# ZONE BUS TRACE #") - print("##################") - print() - await zone_bus_trace(client) - print() - print() + for heading, function in ( + ('FETCH ZONE FEEDERS', fetch_zone_feeders), + ("SPUR ISOLATION", isolate_spur_current), + ("ZONE BUS TRACE", zone_bus_trace) + ): + print(f"\n\n######################\n# {heading} #\n######################\n\n") + await function(client) async def fetch_zone_feeders(client: NetworkConsumerClient): @@ -57,7 +45,12 @@ async def fetch_zone_feeders(client: NetworkConsumerClient): substation = hierarchy.substations["BAS"] for feeder in substation.feeders: print(f" {feeder.mrid}...") - await client.get_equipment_container(feeder.mrid, Feeder, include_energizing_containers=True, include_energized_containers=True) + await client.get_equipment_container( + feeder.mrid, + Feeder, + include_energizing_containers=IncludedEnergizingContainers.INCLUDE_ENERGIZED_FEEDERS, + include_energized_containers=IncludedEnergizedContainers.INCLUDE_ENERGIZING_FEEDERS + ) print("CPM feeders fetched.") @@ -83,29 +76,29 @@ async def isolate_spur_current(client: NetworkConsumerClient): def log_spur(desc: str, switch: Switch, tx: PowerTransformer): - print("==========================") - print(desc) - print( - f" {str(switch)}: is_normally_open={switch.is_normally_open()}, " - f"is_open={switch.is_open()}, " - f"normal_feeders={[it.mrid for it in switch.normal_feeders]}, " - f"current_feeders={[it.mrid for it in switch.current_feeders]}" - ) - print( - f" {str(tx)}: nominal_phases={[it.phases.name for it in tx.terminals]}, " - f"normal_phases={[it.normal_phases.as_phase_code().name for it in tx.terminals]}, " - f"current_phases={[it.current_phases.as_phase_code().name for it in tx.terminals]}, " - f"normal_feeders={[it.mrid for it in tx.normal_feeders]}, " - f"current_feeders={[it.mrid for it in tx.current_feeders]}" + print(f"==========================\n{desc}" + f"\n {str(switch)}: is_normally_open={switch.is_normally_open()}, " + f"is_open={switch.is_open()}, " + f"normal_feeders={[it.mrid for it in switch.normal_feeders]}, " + f"current_feeders={[it.mrid for it in switch.current_feeders]}" + f"\n {str(tx)}: nominal_phases={[it.phases.name for it in tx.terminals]}, " + f"normal_phases={[it.normal_phases.as_phase_code().name for it in tx.terminals]}, " + f"current_phases={[it.current_phases.as_phase_code().name for it in tx.terminals]}, " + f"normal_feeders={[it.mrid for it in tx.normal_feeders]}, " + f"current_feeders={[it.mrid for it in tx.current_feeders]}" + "\n==========================" ) - print("==========================") async def zone_bus_trace(client: NetworkConsumerClient): - feeder_head_terminals = [it.normal_head_terminal for it in client.service.objects(Feeder) if - it.normal_head_terminal is not None and it.normal_head_terminal.conducting_equipment is not None] - feeder_heads = [it.conducting_equipment for it in feeder_head_terminals] - feeder_head_other_terms = [ot for it in feeder_head_terminals for ot in it.other_terminals()] + feeder_head_terminals = [] + feeder_heads = [] + feeder_head_other_terms = [] + for feeder in client.service.objects(Feeder): + if (head_terminal := feeder.normal_head_terminal) and head_terminal.conducting_equipment: + feeder_head_terminals.append(head_terminal) + feeder_heads.append(head_terminal.conducting_equipment) + feeder_head_other_terms.extend(head_terminal.other_terminals()) print(f"creating bus for {[feeder.mrid for it in feeder_heads for feeder in it.normal_feeders]}...") # There is no subtrans in the model we pulled down so create a zone bus for all the feeders. @@ -134,47 +127,50 @@ async def zone_bus_trace(client: NetworkConsumerClient): async def log_bus(desc: str, bus: ConductingEquipment, feeder_heads: List[ConductingEquipment]): - print("==========================") - print(desc) + print(f"==========================\n{desc}") # we run a trace on the assumption that the real model may have more equipment between the bus and the feeder heads. # e.g. other minor busbars or ac line segments - trace = tracing.connected_equipment_trace() open_heads: List[ConductingEquipment] = [] closed_heads: List[ConductingEquipment] = [] normally_open_heads: List[ConductingEquipment] = [] normally_closed_heads: List[ConductingEquipment] = [] # stop at all feeder heads - async def stop_on_feeder_heads(step: ConductingEquipmentStep) -> bool: - return isinstance(step.conducting_equipment, Switch) and step.conducting_equipment in feeder_heads + def stop_on_feeder_heads(step: NetworkTraceStep, _: StepContext) -> bool: + return isinstance(step.path.to_equipment, Switch) and step.path.to_equipment in feeder_heads # stop at transformers to prevent tracing out of this zone into others - async def stop_on_transformers(step: ConductingEquipmentStep) -> bool: - return isinstance(step.conducting_equipment, PowerTransformer) + def stop_on_transformers(step: NetworkTraceStep, _: StepContext) -> bool: + return isinstance(step.path.to_equipment, PowerTransformer) # sort feeder heads based on state - async def sort_feeder_heads(step: ConductingEquipmentStep): - if isinstance(step.conducting_equipment, Switch): - if step.conducting_equipment.is_open(): - open_heads.append(step.conducting_equipment) + def sort_feeder_heads(step: NetworkTraceStep, _: StepContext) -> None: + to_equipment = step.path.to_equipment + if isinstance(to_equipment, Switch): + if to_equipment.is_open(): + open_heads.append(to_equipment) else: - closed_heads.append(step.conducting_equipment) - if step.conducting_equipment.is_normally_open(): - normally_open_heads.append(step.conducting_equipment) + closed_heads.append(to_equipment) + if to_equipment.is_normally_open(): + normally_open_heads.append(to_equipment) else: - normally_closed_heads.append(step.conducting_equipment) + normally_closed_heads.append(to_equipment) - trace.add_stop_condition(stop_on_feeder_heads) - trace.add_stop_condition(stop_on_transformers) - trace.if_stopping(sort_feeder_heads) + await ( + Tracing.network_trace() + .add_stop_condition(stop_on_feeder_heads) + .add_stop_condition(stop_on_transformers) + .if_stopping(sort_feeder_heads) + ).run(start=bus) - await trace.run_from(bus) - print(f" disconnected feeders: {[feeder.mrid for it in open_heads for feeder in it.normal_feeders]}") - print(f" connected feeders: {[feeder.mrid for it in closed_heads for feeder in it.normal_feeders]}") - print(f" normally disconnected feeders: {[feeder.mrid for it in normally_open_heads for feeder in it.normal_feeders]}") - print(f" normally connected feeders: {[feeder.mrid for it in normally_closed_heads for feeder in it.normal_feeders]}") - print("==========================") + print( + f" disconnected feeders: {[feeder.mrid for it in open_heads for feeder in it.normal_feeders]}" + f"\n connected feeders: {[feeder.mrid for it in closed_heads for feeder in it.normal_feeders]}" + f"\n normally disconnected feeders: {[feeder.mrid for it in normally_open_heads for feeder in it.normal_feeders]}" + f"\n normally connected feeders: {[feeder.mrid for it in normally_closed_heads for feeder in it.normal_feeders]}" + "\n==========================" + ) async def run_swap_feeder(client: NetworkConsumerClient): @@ -216,12 +212,8 @@ def clear_feeders(feeders: Set[Feeder]): # remove the phases and feeders to show the difference in open/normal state for feeder in feeders: print(f"removing phases from {feeder.mrid}...") - # should use `await remove_phases().run(feeder.normal_head_terminal)`, but it is not working (or just really slow) for some reason... - for equip in feeder.equipment: - if isinstance(equip, ConductingEquipment): - for term in equip.terminals: - term.normal_phases.phase_status = 0 - term.current_phases.phase_status = 0 + Tracing.remove_phases().run(start=feeder.normal_head_terminal) + print(f"phases removed, removing equipment...") for equip in feeder.equipment: equip.clear_containers() @@ -235,9 +227,9 @@ async def recalculate_feeders(feeders: Set[Feeder]): # recalculate the phases and feeders with the new switch state. for feeder in feeders: print(f"assigning phases to {feeder.mrid}...") - await set_phases().run_with_terminal(feeder.normal_head_terminal) + await Tracing.set_phases().run_with_terminal(feeder.normal_head_terminal) print(f"phases assigned, assigning equipment...") - await assign_equipment_to_feeders().run_feeder(feeder) + await Tracing.assign_equipment_to_feeders().run_feeder(feeder) print(f"equipment assigned.") @@ -256,7 +248,7 @@ async def main(): raise TypeError("you must provided the CLIENT_ID, username, password, host and port to connect") # noinspection PyTypeChecker - async with connect_with_password(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) as secure_channel: + async with connect_with_password(*sys.argv[1:]) as secure_channel: await run_simple(NetworkConsumerClient(secure_channel)) await run_swap_feeder(NetworkConsumerClient(secure_channel)) From 5cc10bfac92b91942dc9ca4861e02ed3917c205e Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 15:41:40 +1000 Subject: [PATCH 05/12] minor formatting changes Signed-off-by: Max Chesterfield --- src/zepben/examples/dsub_from_nmi.py | 83 ++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/zepben/examples/dsub_from_nmi.py diff --git a/src/zepben/examples/dsub_from_nmi.py b/src/zepben/examples/dsub_from_nmi.py new file mode 100644 index 0000000..665f957 --- /dev/null +++ b/src/zepben/examples/dsub_from_nmi.py @@ -0,0 +1,83 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +""" +Example trace showing methods to traverse upstream of a given `IdentifiedObject` to find the first +occurrence of another specified `IdentifiedObject` +""" + +import asyncio +import json + +from zepben.evolve import ( + NetworkStateOperators, NetworkTraceActionType, NetworkTraceStep, StepContext, + NetworkConsumerClient, ConductingEquipment, connect_with_token +) +from zepben.evolve import PowerTransformer, UsagePoint, Tracing, Switch +from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS + + +with open("config.json") as f: + c = json.loads(f.read()) + + +def _trace(start_item, results, stop_condition): + """Returns a `NetworkTrace` configured with our parameters""" + + def step_action(step: NetworkTraceStep, context: StepContext): + if context.is_stopping: # if the trace is stopping, we have found the equipment we're looking for + results.append(step.path.to_equipment) + + state_operators = NetworkStateOperators.NORMAL + + return ( + Tracing.network_trace( + network_state_operators=state_operators, + action_step_type=NetworkTraceActionType.ALL_STEPS + ).add_condition(state_operators.upstream()) + .add_stop_condition(stop_condition) + .add_step_action(step_action) + .add_start_item(start_item) + ) + + +async def main(mrid: str, feeder_mrid: str): + channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + client = NetworkConsumerClient(channel) + await client.get_equipment_container(feeder_mrid, include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS) + network = client.service + + try: + usage_point = network.get(mrid, UsagePoint) + # get the `ConductingEquipment` from the `UsagePoint` + start_item = next(filter(lambda ce: isinstance(ce, ConductingEquipment), usage_point.equipment)) + except TypeError: + start_item = network.get(mrid, ConductingEquipment) + + results = [] + + # Get DSUB from which any given customer is supplied from using a basic upstream trace + def dsub_stop_condition(step: NetworkTraceStep, _: StepContext): + return isinstance(step.path.to_equipment, PowerTransformer) + + # Get Circuit Breaker from which any given customer is supplied from using a basic upstream trace + # Uncomment stop condition below to use + def circuit_breaker_stop_condition(step: NetworkTraceStep, _: StepContext): + return isinstance(step.path.to_equipment, Switch) + + await _trace( + start_item=start_item, + results=results, + stop_condition=dsub_stop_condition, + # stop_condition=circuit_breaker_stop_condition, + ).run() + + print(results) + + +if __name__ == "__main__": + # EnergyConsumer: 50763684 + asyncio.run(main(mrid='4310990779', feeder_mrid='RW1292')) From d1c5c00ca21d106125c390aa1f287f82ac9305c6 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 16:03:11 +1000 Subject: [PATCH 06/12] Formatting changes, slightly more descriptive notes (maybe) Signed-off-by: Max Chesterfield --- src/zepben/examples/examining_connectivity.py | 179 +++++++++++------- 1 file changed, 108 insertions(+), 71 deletions(-) diff --git a/src/zepben/examples/examining_connectivity.py b/src/zepben/examples/examining_connectivity.py index 7dc8886..53b1b7c 100644 --- a/src/zepben/examples/examining_connectivity.py +++ b/src/zepben/examples/examining_connectivity.py @@ -1,67 +1,93 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from zepben.evolve import EnergySource, AcLineSegment, Fuse, PowerTransformer, Breaker, EnergyConsumer, NetworkService, Terminal, connected_equipment, \ - ConductingEquipment, PhaseCode, connected_terminals, ConnectivityResult - -# This example explores how to examine the immediate connectivity of equipment. -# We will build a simple, linear network to examine: - -# source consumer -# | | -# line line -# | | -# fuse breaker -# | | -# transformer fuse -# | | -# +-----------+ - -es_t = Terminal(mrid="es-t") -es = EnergySource(mrid="es", terminals=[es_t]) - -hv_line_t1, hv_line_t2 = Terminal(mrid="hv_line_t1"), Terminal(mrid="hv_line_t2") -hv_line = AcLineSegment(mrid="hv_line", terminals=[hv_line_t1, hv_line_t2]) - -hv_fuse_t1, hv_fuse_t2 = Terminal(mrid="hv_fuse_t1"), Terminal(mrid="hv_fuse_t2") -hv_fuse = Fuse(mrid="hv_fuse", terminals=[hv_fuse_t1, hv_fuse_t2]) - -tx_t1, tx_t2 = Terminal(mrid="tx_t1"), Terminal(mrid="tx_t2", phases=PhaseCode.ABCN) -tx = PowerTransformer(mrid="tx", terminals=[tx_t1, tx_t2]) - -lv_fuse_t1, lv_fuse_t2 = Terminal(mrid="lv_fuse_t1", phases=PhaseCode.ABCN), Terminal(mrid="lv_fuse_t2", phases=PhaseCode.ABCN) -lv_fuse = Fuse(mrid="lv_fuse", terminals=[lv_fuse_t1, lv_fuse_t2]) - -breaker_t1, breaker_t2 = Terminal(mrid="breaker_t1", phases=PhaseCode.ABCN), Terminal(mrid="breaker_t2", phases=PhaseCode.BN) -breaker = Breaker(mrid="breaker", terminals=[breaker_t1, breaker_t2]) - -lv_line_t1, lv_line_t2 = Terminal(mrid="lv_line_t1", phases=PhaseCode.BN), Terminal(mrid="lv_line_t2", phases=PhaseCode.BN) -lv_line = AcLineSegment(mrid="lv_line", terminals=[lv_line_t1, lv_line_t2]) - -ec_t = Terminal(mrid="ec_t", phases=PhaseCode.BN) -ec = EnergyConsumer(mrid="ec", terminals=[ec_t]) - -network = NetworkService() -for io in [es_t, es, hv_line_t1, hv_line_t2, hv_line, hv_fuse_t1, hv_fuse_t2, hv_fuse, tx_t1, tx_t2, tx, lv_fuse_t1, lv_fuse_t2, lv_fuse, breaker_t1, - breaker_t2, breaker, lv_line_t1, lv_line_t2, lv_line, ec_t, ec]: - network.add(io) - -network.connect_terminals(es_t, hv_line_t1) -network.connect_terminals(hv_line_t2, hv_fuse_t1) -network.connect_terminals(hv_fuse_t2, tx_t1) -network.connect_terminals(tx_t2, lv_fuse_t1) -network.connect_terminals(lv_fuse_t2, breaker_t1) -network.connect_terminals(breaker_t2, lv_line_t1) -network.connect_terminals(lv_line_t2, ec_t) - - -def fancy_print_connectivity_result(connectivity_result: ConnectivityResult): - print(f"\t{connectivity_result.from_terminal} to {connectivity_result.to_terminal}") - - terminal_str_len = len(str(connectivity_result.from_terminal)) - for core_path in connectivity_result.nominal_phase_paths: +from zepben.evolve import ( + EnergySource, AcLineSegment, Fuse, PowerTransformer, Breaker, EnergyConsumer, NetworkService, + Terminal, connected_equipment, ConductingEquipment, PhaseCode, connected_terminals, + ConnectivityResult +) + +def build_network() -> NetworkService: + """ + This function will return a network model resembling the below. + + source consumer + | | + line line + | | + fuse breaker + | | + transformer fuse + | | + +-----------+ + """ + + # We create the objects, and their Terminals + _es = EnergySource(mrid="es", terminals=[ + Terminal(mrid="es-t") + ]) + + _hv_line = AcLineSegment(mrid="hv_line", terminals=[ + Terminal(mrid="hv_line_t1"), + Terminal(mrid="hv_line_t2") + ]) + + _hv_fuse = Fuse(mrid="hv_fuse", terminals=[ + Terminal(mrid="hv_fuse_t1"), + Terminal(mrid="hv_fuse_t2") + ]) + + _tx = PowerTransformer(mrid="tx", terminals=[ + Terminal(mrid="tx_t1"), + Terminal(mrid="tx_t2", phases=PhaseCode.ABCN) + ]) + + _lv_fuse = Fuse(mrid="lv_fuse", terminals=[ + Terminal(mrid="lv_fuse_t1", phases=PhaseCode.ABCN), + Terminal(mrid="lv_fuse_t2", phases=PhaseCode.ABCN) + ]) + + _breaker = Breaker(mrid="breaker", terminals=[ + Terminal(mrid="breaker_t1", phases=PhaseCode.ABCN), + Terminal(mrid="breaker_t2", phases=PhaseCode.BN) + ]) + + _lv_line = AcLineSegment(mrid="lv_line", terminals=[ + Terminal(mrid="lv_line_t1", phases=PhaseCode.BN), + Terminal(mrid="lv_line_t2", phases=PhaseCode.BN) + ]) + + _ec = EnergyConsumer(mrid="ec", terminals=[ + Terminal(mrid="ec_t", phases=PhaseCode.BN) + ]) + + # Now we add the objects and their terminals to the network + network = NetworkService() + for io in (_es, _hv_line, _hv_fuse, _tx, _lv_fuse, _breaker, _lv_line, _ec): + network.add(io) # add the object + for terminal in io.terminals: # iterate over Terminals + network.add(terminal) # add them too + + # Power grids aren't much use if the equipment in them isn't connected to anything, + # Lets connect them. + network.connect_terminals(network['es_t'], network['hv_line_t1']) + network.connect_terminals(network['hv_line_t2'], network['hv_fuse_t1']) + network.connect_terminals(network['hv_fuse_t2'], network['tx_t1']) + network.connect_terminals(network['tx_t2'], network['lv_fuse_t1']) + network.connect_terminals(network['lv_fuse_t2'], network['breaker_t1']) + network.connect_terminals(network['breaker_t2'], network['lv_line_t1']) + network.connect_terminals(network['lv_line_t2'], network['ec_t']) + + return network + + +def fancy_print_connectivity_result(_connectivity_result: ConnectivityResult): + print(f"\t{_connectivity_result.from_terminal} to {_connectivity_result.to_terminal}") + + terminal_str_len = len(str(_connectivity_result.from_terminal)) + for core_path in _connectivity_result.nominal_phase_paths: print(f"\t{core_path.from_phase.name:>{terminal_str_len}}----{core_path.to_phase.name}") @@ -70,18 +96,29 @@ def fancy_print_connected_equipment(equipment: ConductingEquipment, phases=None) print(f"Connectivity results for {equipment} on phases {phases}:") else: print(f"Connectivity results for {equipment}:") - for connectivity_result in connected_equipment(equipment, phases): - fancy_print_connectivity_result(connectivity_result) + for _connectivity_result in connected_equipment(equipment, phases): + fancy_print_connectivity_result(_connectivity_result) print() -# connected_equipment(equipment, phases) will get all connections between equipment cores matching one of the requested phases. -# The connected equipment does not need to connect via all specified phases to appear in the list of connectivity results. -fancy_print_connected_equipment(tx) -fancy_print_connected_equipment(tx, phases=PhaseCode.N) -fancy_print_connected_equipment(breaker, phases=PhaseCode.BC) +if __name__ == '__main__': + # This example explores how to examine the immediate connectivity of equipment. + # We will build a simple, linear network to examine: + n = build_network() + + # Get references to the ConductingEquipment we are interested in from the network + tx = n['tx'] + breaker = n['breaker'] + lv_fuse_t2 = n['lv_fuse_t2'] -# connected_terminals is essentially connected_equipment where only one terminal is considered. -print(f"Connectivity results for terminal {lv_fuse_t2} on phases {PhaseCode.ACN}:") -for connectivity_result in connected_terminals(lv_fuse_t2, PhaseCode.ACN): - fancy_print_connectivity_result(connectivity_result) + # connected_equipment(equipment, phases) will get all connections between equipment cores + # matching one of the requested phases. The connected equipment does not need to connect + # via all specified phases to appear in the list of connectivity results. + fancy_print_connected_equipment(tx) + fancy_print_connected_equipment(tx, phases=PhaseCode.N) + fancy_print_connected_equipment(breaker, phases=PhaseCode.BC) + + # connected_terminals is essentially connected_equipment where only one terminal is considered. + print(f"Connectivity results for terminal {lv_fuse_t2} on phases {PhaseCode.ACN}:") + for connectivity_result in connected_terminals(lv_fuse_t2, PhaseCode.ACN): + fancy_print_connectivity_result(connectivity_result) From e480e58635cc6357126bbb11e41961e35762a04a Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 16:03:38 +1000 Subject: [PATCH 07/12] license date update Signed-off-by: Max Chesterfield --- src/zepben/examples/current_state_manipulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zepben/examples/current_state_manipulations.py b/src/zepben/examples/current_state_manipulations.py index 689b140..d7137bc 100644 --- a/src/zepben/examples/current_state_manipulations.py +++ b/src/zepben/examples/current_state_manipulations.py @@ -1,4 +1,4 @@ -# Copyright 2023 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this From 3fc982a216ac5aba4dbc4ecf202b0ebdc65a8b80 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 16:33:14 +1000 Subject: [PATCH 08/12] reformat Signed-off-by: Max Chesterfield --- src/zepben/examples/fetching_network_model.py | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/src/zepben/examples/fetching_network_model.py b/src/zepben/examples/fetching_network_model.py index dcdeed0..2216e1f 100644 --- a/src/zepben/examples/fetching_network_model.py +++ b/src/zepben/examples/fetching_network_model.py @@ -1,23 +1,25 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import asyncio import json +from collections import defaultdict -from zepben.evolve import Conductor, PowerTransformer, ConductingEquipment, EnergyConsumer, Switch, \ - connect_with_token, NetworkConsumerClient -from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS, INCLUDE_ENERGIZED_FEEDERS, INCLUDE_ENERGIZING_SUBSTATIONS, \ - INCLUDE_ENERGIZING_FEEDERS - -with open("config.json") as f: - c = json.loads(f.read()) +from zepben.evolve import ( + Conductor, PowerTransformer, ConductingEquipment, EnergyConsumer, Switch, connect_with_token, NetworkConsumerClient +) +from zepben.protobuf.nc.nc_requests_pb2 import ( + INCLUDE_ENERGIZED_LV_FEEDERS, INCLUDE_ENERGIZED_FEEDERS, INCLUDE_ENERGIZING_SUBSTATIONS, INCLUDE_ENERGIZING_FEEDERS +) async def main(): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EWB..") + with open("config.json") as f: + c = json.loads(f.read()) channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) feeder_mrid = "WD24" print(f"Fetching {feeder_mrid}") @@ -27,61 +29,74 @@ async def main(): network = client.service # Fetch feeder and all its LvFeeders - await client.get_equipment_container(feeder_mrid, include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS) + await client.get_equipment_container( + feeder_mrid, + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) - print() - print(f"Total Number of objects: {client.service.len_of()}") - types = set(type(x) for x in network.objects(ConductingEquipment)) - for t in types: - print(f"Number of {t.__name__}'s = {len(list(network.objects(t)))}") + print(f"\nTotal Number of objects: {client.service.len_of()}") + # build a dictionary mapping of all objects returned organised by type. + # e.g. {type(ConductingEquipment): [ce, ...]} + objects_by_type = defaultdict(list) + for ce in network.objects(ConductingEquipment): + objects_by_type[type(ce)].append(ce) - total_length = 0 - for conductor in network.objects(Conductor): - if conductor.length is not None: - total_length += conductor.length + for t, objects in objects_by_type.items(): + print(f"Number of {t.__name__}'s = {len(objects)}") - print() - print(f"Total conductor length in {feeder_mrid}: {total_length:.3f}m") + total_length = sum(c.length for c in network.objects(Conductor) if c.length) + print(f"\nTotal conductor length in {feeder_mrid}: {total_length:.3f}m") - print() feeder = network.get(feeder_mrid) - print(f"{feeder.mrid} Transformers:") + print(f"\n{feeder.mrid} Transformers:") for eq in feeder.equipment: if isinstance(eq, PowerTransformer): print(f" {eq} - Vector Group: {eq.vector_group.short_name}, Function: {eq.function.short_name}") - print() - print() - print(f"{feeder_mrid} Energy Consumers:") + print(f"\n\n{feeder_mrid} Energy Consumers:") for ec in network.objects(EnergyConsumer): print(f" {ec} - Real power draw: {ec.q}W, Reactive power draw: {ec.p}VAr") - print() - print(f"{feeder_mrid} Switches:") + print(f"\n{feeder_mrid} Switches:") for switch in network.objects(Switch): print(f" {switch} - Open status: {switch.get_state():04b}") # === Some other examples of fetching containers === # Fetch substation equipment and include equipment from HV/MV feeders powered by it - await client.get_equipment_container("substation ID", include_energized_containers=INCLUDE_ENERGIZED_FEEDERS) + await client.get_equipment_container( + "substation ID", + include_energized_containers=INCLUDE_ENERGIZED_FEEDERS + ) # Same as above, but also fetch equipment from LV feeders powered by the HV/MV feeders - await client.get_equipment_container("substation ID", include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS) + await client.get_equipment_container( + "substation ID", + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) # Fetch feeder equipment without fetching any additional equipment from powering/powered containers await client.get_equipment_container("feeder ID") - # Fetch HV/MV feeder equipment, the equipment from the substation powering it, and the equipment from the LV feeders it powers - await client.get_equipment_container("feeder ID", - include_energizing_containers=INCLUDE_ENERGIZING_SUBSTATIONS, - include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS) + # Fetch HV/MV feeder equipment, the equipment from the substation powering it, + # and the equipment from the LV feeders it powers + await client.get_equipment_container( + "feeder ID", + include_energizing_containers=INCLUDE_ENERGIZING_SUBSTATIONS, + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) # Fetch LV feeder equipment and include equipment from HV/MV feeders powering it - await client.get_equipment_container("LV feeder ID", include_energizing_containers=INCLUDE_ENERGIZING_FEEDERS) + await client.get_equipment_container( + "LV feeder ID", + include_energizing_containers=INCLUDE_ENERGIZING_FEEDERS + ) # Same as above, but also fetch equipment from the substations powering the HV/MV feeders - await client.get_equipment_container("LV feeder ID", include_energizing_containers=INCLUDE_ENERGIZING_SUBSTATIONS) + await client.get_equipment_container( + "LV feeder ID", + include_energizing_containers=INCLUDE_ENERGIZING_SUBSTATIONS + ) if __name__ == "__main__": From 9590e2d4055417fb4751acd1e7f11d3716394150 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 16:33:46 +1000 Subject: [PATCH 09/12] wrong import Signed-off-by: Max Chesterfield --- src/zepben/examples/current_state_manipulations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zepben/examples/current_state_manipulations.py b/src/zepben/examples/current_state_manipulations.py index d7137bc..6502167 100644 --- a/src/zepben/examples/current_state_manipulations.py +++ b/src/zepben/examples/current_state_manipulations.py @@ -13,7 +13,7 @@ BusbarSection, ConductingEquipment, Breaker, EquipmentContainer, StepContext, NetworkTraceStep ) -from zepben.protobuf.nc.nc_requests_pb2 import IncludedEnergizedContainers, IncludedEnergizingContainers +from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_FEEDERS, INCLUDE_ENERGIZING_FEEDERS """ Primary question to answer/example for: @@ -48,8 +48,8 @@ async def fetch_zone_feeders(client: NetworkConsumerClient): await client.get_equipment_container( feeder.mrid, Feeder, - include_energizing_containers=IncludedEnergizingContainers.INCLUDE_ENERGIZED_FEEDERS, - include_energized_containers=IncludedEnergizedContainers.INCLUDE_ENERGIZING_FEEDERS + include_energizing_containers=INCLUDE_ENERGIZED_FEEDERS, + include_energized_containers=INCLUDE_ENERGIZING_FEEDERS ) print("CPM feeders fetched.") From eaa69ade27aee46c46a4c6f59c5f767bedcf3fa3 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Tue, 10 Jun 2025 16:33:54 +1000 Subject: [PATCH 10/12] license Signed-off-by: Max Chesterfield --- src/zepben/examples/fetching_network_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zepben/examples/fetching_network_hierarchy.py b/src/zepben/examples/fetching_network_hierarchy.py index 9c659ae..e6f2af4 100644 --- a/src/zepben/examples/fetching_network_hierarchy.py +++ b/src/zepben/examples/fetching_network_hierarchy.py @@ -1,4 +1,4 @@ -# Copyright 2023 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this From c4a0df4764821b65f9a8f8a8130b6b24fe482d5d Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 11 Jun 2025 00:57:26 +1000 Subject: [PATCH 11/12] Maybe i got carried away and forgot to commit for a while Signed-off-by: Max Chesterfield --- .gitignore | 4 + setup.py | 4 +- .../find_isolation_section_from_equipment.py | 65 ++ .../examples/ieee_13_node_test_feeder.py | 242 +++++-- .../isolation_equipment_between_nodes.py | 76 +++ .../examples/list_ewb_network_models.py | 11 +- .../examples/network_service_interactions.py | 84 +-- .../examples/request_power_factory_models.py | 244 ++++--- src/zepben/examples/tracing.py | 633 ++++++++++-------- .../tracing_conductor_type_by_lv_circuit.py | 42 +- src/zepben/examples/tracing_example.py | 49 +- .../translating_to_pandapower_model.py | 10 +- 12 files changed, 916 insertions(+), 548 deletions(-) create mode 100644 src/zepben/examples/find_isolation_section_from_equipment.py create mode 100644 src/zepben/examples/isolation_equipment_between_nodes.py diff --git a/.gitignore b/.gitignore index 9e990a6..33abbbb 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,7 @@ dmypy.json /todo.md docs/node_modules docs/build + +src/zepben/examples/config.json + +*.crt diff --git a/setup.py b/setup.py index fa231c0..68caa4e 100644 --- a/setup.py +++ b/setup.py @@ -10,10 +10,10 @@ author_email="oss@zepben.com", license="MPL 2.0", classifiers=[ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Operating System :: OS Independent" ], packages=find_namespace_packages(where="src"), diff --git a/src/zepben/examples/find_isolation_section_from_equipment.py b/src/zepben/examples/find_isolation_section_from_equipment.py new file mode 100644 index 0000000..4bea668 --- /dev/null +++ b/src/zepben/examples/find_isolation_section_from_equipment.py @@ -0,0 +1,65 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +""" +Example trace showing method to traverse outwards from any given `IdentifiableObject` to the next +`Switch` object, and build a list of all contained equipment (isolate-able section) +""" + +import asyncio +import json + +from zepben.evolve import ( + NetworkStateOperators, NetworkTraceActionType, NetworkTraceStep, StepContext, + NetworkConsumerClient, AcLineSegment, connect_with_token, stop_at_open +) +from zepben.evolve import Tracing, Switch +from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS + + +async def main(conductor_mrid: str, feeder_mrid: str): + with open("config.json") as f: + c = json.loads(f.read()) + + channel = connect_with_token( + host=c["host"], + access_token=c["access_token"], + rpc_port=c["rpc_port"] + ) + client = NetworkConsumerClient(channel) + await client.get_equipment_container( + feeder_mrid, include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) + network = client.service + + hv_acls = network.get(conductor_mrid, AcLineSegment) + + found_equip = set() + + def queue_condition(step: NetworkTraceStep, context: StepContext, _, __): + """Queue the next step unless it's a `Switch`""" + return not isinstance(step.path.to_equipment, Switch) + + def step_action(step: NetworkTraceStep, context: StepContext): + """Add to our list of equipment, and equipment stepped on during this trace""" + found_equip.add(step.path.to_equipment.mrid) + + await ( + Tracing.network_trace( + network_state_operators=NetworkStateOperators.NORMAL, + action_step_type=NetworkTraceActionType.ALL_STEPS + ).add_condition(stop_at_open()) + .add_queue_condition(queue_condition) + .add_step_action(step_action) + .add_start_item(hv_acls) + ).run() + + # print a list of all mRID's for all equipment in the isolation area. + print(found_equip) + + +if __name__ == "__main__": + asyncio.run(main(conductor_mrid='50434998', feeder_mrid='RW1292')) diff --git a/src/zepben/examples/ieee_13_node_test_feeder.py b/src/zepben/examples/ieee_13_node_test_feeder.py index f677e5d..c3bad9e 100644 --- a/src/zepben/examples/ieee_13_node_test_feeder.py +++ b/src/zepben/examples/ieee_13_node_test_feeder.py @@ -1,4 +1,4 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -6,9 +6,11 @@ from typing import Tuple import numpy -from zepben.evolve import AcLineSegment, Disconnector, PowerTransformer, TransformerFunctionKind, NetworkService, Terminal, PowerTransformerEnd, EnergyConsumer, \ - PerLengthSequenceImpedance, PhaseCode, EnergyConsumerPhase, SinglePhaseKind, LinearShuntCompensator, ShuntCompensatorInfo, PhaseShuntConnectionKind, Feeder, \ - LvFeeder, BaseVoltage, EnergySource, Breaker +from zepben.evolve import ( + AcLineSegment, Disconnector, PowerTransformer, TransformerFunctionKind, NetworkService, Terminal, + PowerTransformerEnd, EnergyConsumer, PerLengthSequenceImpedance, PhaseCode, EnergyConsumerPhase, + SinglePhaseKind, LinearShuntCompensator, ShuntCompensatorInfo, Feeder, LvFeeder, BaseVoltage, Breaker +) __all__ = ["network"] @@ -31,103 +33,235 @@ vr_650_632_t1, vr_650_632_t2 = Terminal(mrid="vr_650_632_t1"), Terminal(mrid="vr_650_632_t2") vr_650_632_e1 = PowerTransformerEnd(mrid="vr_650_632_e1", terminal=vr_650_632_t1, rated_u=mv.nominal_voltage) vr_650_632_e2 = PowerTransformerEnd(mrid="vr_650_632_e2", terminal=vr_650_632_t2, rated_u=mv.nominal_voltage) -vr_650_632 = PowerTransformer(mrid="vr_650_632", function=TransformerFunctionKind.voltageRegulator, terminals=[vr_650_632_t1, vr_650_632_t2], - power_transformer_ends=[vr_650_632_e1, vr_650_632_e2]) - -l_632_645_t1, l_632_645_t2 = Terminal(mrid="l_632_645_t1", phases=PhaseCode.BCN), Terminal(mrid="l_632_645_t2", phases=PhaseCode.BCN) -l_632_645 = AcLineSegment(mrid="l_632_645", length=500 * METRES_PER_FOOT, terminals=[l_632_645_t1, l_632_645_t2], base_voltage=mv) - -l_632_633_t1, l_632_633_t2 = Terminal(mrid="l_632_633_t1", phases=PhaseCode.ABCN), Terminal(mrid="l_632_633_t2", phases=PhaseCode.ABCN) -l_632_633 = AcLineSegment(mrid="l_632_633", length=500 * METRES_PER_FOOT, terminals=[l_632_633_t1, l_632_633_t2], base_voltage=mv) - -tx_633_634_t1, tx_633_634_t2 = Terminal(mrid="tx_633_634_t1", phases=PhaseCode.ABCN), Terminal(mrid="tx_633_634_t2", phases=PhaseCode.ABCN) -tx_633_634_e1 = PowerTransformerEnd(mrid="tx_633_634_e1", terminal=tx_633_634_t1, rated_u=mv.nominal_voltage) -tx_633_634_e2 = PowerTransformerEnd(mrid="tx_633_634_e2", terminal=tx_633_634_t2, rated_u=lv.nominal_voltage) -tx_633_634 = PowerTransformer(mrid="tx_633_634", terminals=[tx_633_634_t1, tx_633_634_t2], power_transformer_ends=[tx_633_634_e1, tx_633_634_e2]) - -l_645_646_t1, l_645_646_t2 = Terminal(mrid="l_645_646_t1", phases=PhaseCode.BCN), Terminal(mrid="l_645_646_t2", phases=PhaseCode.BCN) -l_645_646 = AcLineSegment(mrid="l_645_646", length=300 * METRES_PER_FOOT, terminals=[l_645_646_t1, l_645_646_t2], base_voltage=mv) - -l_650_632_t1, l_650_632_t2 = Terminal(mrid="l_650_632_t1", phases=PhaseCode.ABCN), Terminal(mrid="l_650_632_t2", phases=PhaseCode.ABCN) -l_650_632 = AcLineSegment(mrid="l_650_632", length=2000 * METRES_PER_FOOT, terminals=[l_650_632_t1, l_650_632_t2], base_voltage=mv) - -l_684_652_t1, l_684_652_t2 = Terminal(mrid="l_684_652_t1", phases=PhaseCode.AN), Terminal(mrid="l_684_652_t2", phases=PhaseCode.AN) -l_684_652 = AcLineSegment(mrid="l_684_652", length=800 * METRES_PER_FOOT, terminals=[l_684_652_t1, l_684_652_t2], base_voltage=mv) - -l_632_671_t1, l_632_671_t2 = Terminal(mrid="l_632_671_t1", phases=PhaseCode.ABCN), Terminal(mrid="l_632_671_t2", phases=PhaseCode.ABCN) -l_632_671 = AcLineSegment(mrid="l_632_671", length=2000 * METRES_PER_FOOT, terminals=[l_632_671_t1, l_632_671_t2], base_voltage=mv) - -l_671_684_t1, l_671_684_t2 = Terminal(mrid="l_671_684_t1", phases=PhaseCode.ACN), Terminal(mrid="l_671_684_t2", phases=PhaseCode.ACN) -l_671_684 = AcLineSegment(mrid="l_671_684", length=300 * METRES_PER_FOOT, terminals=[l_671_684_t1, l_671_684_t2], base_voltage=mv) - -l_671_680_t1, l_671_680_t2 = Terminal(mrid="l_671_680_t1", phases=PhaseCode.ABCN), Terminal(mrid="l_671_680_t2", phases=PhaseCode.ABCN) -l_671_680 = AcLineSegment(mrid="l_671_680", length=1000 * METRES_PER_FOOT, terminals=[l_671_680_t1, l_671_680_t2], base_voltage=mv) - -sw_671_692_t1, sw_671_692_t2 = Terminal(mrid="sw_671_692_t1", phases=PhaseCode.ABCN), Terminal(mrid="sw_671_692_t2", phases=PhaseCode.ABCN) -sw_671_692 = Disconnector(mrid="sw_671_692", terminals=[sw_671_692_t1, sw_671_692_t2], base_voltage=mv) - -l_684_611_t1, l_684_611_t2 = Terminal(mrid="l_684_611_t1", phases=PhaseCode.CN), Terminal(mrid="l_684_611_t2", phases=PhaseCode.CN) -l_684_611 = AcLineSegment(mrid="l_684_611", length=300 * METRES_PER_FOOT, terminals=[l_684_611_t1, l_684_611_t2], base_voltage=mv) - -l_692_675_t1, l_692_675_t2 = Terminal(mrid="l_692_675_t1", phases=PhaseCode.ABCN), Terminal(mrid="l_692_675_t2", phases=PhaseCode.ABCN) -l_692_675 = AcLineSegment(mrid="l_692_675", length=500 * METRES_PER_FOOT, terminals=[l_692_675_t1, l_692_675_t2], base_voltage=mv) +vr_650_632 = PowerTransformer( + mrid="vr_650_632", + function=TransformerFunctionKind.voltageRegulator, + terminals=[vr_650_632_t1, vr_650_632_t2], + power_transformer_ends=[vr_650_632_e1, vr_650_632_e2] +) + +l_632_645_t1, l_632_645_t2 = (Terminal(mrid="l_632_645_t1", phases=PhaseCode.BCN), + Terminal(mrid="l_632_645_t2", phases=PhaseCode.BCN)) +l_632_645 = AcLineSegment( + mrid="l_632_645", + length=500 * METRES_PER_FOOT, + terminals=[l_632_645_t1, l_632_645_t2], + base_voltage=mv +) + +l_632_633_t1, l_632_633_t2 = (Terminal(mrid="l_632_633_t1", phases=PhaseCode.ABCN), + Terminal(mrid="l_632_633_t2", phases=PhaseCode.ABCN)) +l_632_633 = AcLineSegment( + mrid="l_632_633", + length=500 * METRES_PER_FOOT, + terminals=[l_632_633_t1, l_632_633_t2], + base_voltage=mv +) + +tx_633_634_t1, tx_633_634_t2 = (Terminal(mrid="tx_633_634_t1", phases=PhaseCode.ABCN), + Terminal(mrid="tx_633_634_t2", phases=PhaseCode.ABCN)) +tx_633_634_e1 = PowerTransformerEnd( + mrid="tx_633_634_e1", + terminal=tx_633_634_t1, + rated_u=mv.nominal_voltage +) +tx_633_634_e2 = PowerTransformerEnd( + mrid="tx_633_634_e2", + terminal=tx_633_634_t2, + rated_u=lv.nominal_voltage +) +tx_633_634 = PowerTransformer( + mrid="tx_633_634", + terminals=[tx_633_634_t1, tx_633_634_t2], + power_transformer_ends=[tx_633_634_e1, tx_633_634_e2] +) + +l_645_646_t1, l_645_646_t2 = (Terminal(mrid="l_645_646_t1", phases=PhaseCode.BCN), + Terminal(mrid="l_645_646_t2", phases=PhaseCode.BCN)) +l_645_646 = AcLineSegment( + mrid="l_645_646", + length=300 * METRES_PER_FOOT, + terminals=[l_645_646_t1, l_645_646_t2], + base_voltage=mv +) + +l_650_632_t1, l_650_632_t2 = (Terminal(mrid="l_650_632_t1", phases=PhaseCode.ABCN), + Terminal(mrid="l_650_632_t2", phases=PhaseCode.ABCN)) +l_650_632 = AcLineSegment( + mrid="l_650_632", + length=2000 * METRES_PER_FOOT, + terminals=[l_650_632_t1, l_650_632_t2], + base_voltage=mv +) + +l_684_652_t1, l_684_652_t2 = (Terminal(mrid="l_684_652_t1", phases=PhaseCode.AN), + Terminal(mrid="l_684_652_t2", phases=PhaseCode.AN)) +l_684_652 = AcLineSegment( + mrid="l_684_652", + length=800 * METRES_PER_FOOT, + terminals=[l_684_652_t1, l_684_652_t2], + base_voltage=mv +) + +l_632_671_t1, l_632_671_t2 = (Terminal(mrid="l_632_671_t1", phases=PhaseCode.ABCN), + Terminal(mrid="l_632_671_t2", phases=PhaseCode.ABCN)) +l_632_671 = AcLineSegment( + mrid="l_632_671", + length=2000 * METRES_PER_FOOT, + terminals=[l_632_671_t1, l_632_671_t2], + base_voltage=mv +) + +l_671_684_t1, l_671_684_t2 = (Terminal(mrid="l_671_684_t1", phases=PhaseCode.ACN), + Terminal(mrid="l_671_684_t2", phases=PhaseCode.ACN)) +l_671_684 = AcLineSegment( + mrid="l_671_684", + length=300 * METRES_PER_FOOT, + terminals=[l_671_684_t1, l_671_684_t2], + base_voltage=mv +) + +l_671_680_t1, l_671_680_t2 = (Terminal(mrid="l_671_680_t1", phases=PhaseCode.ABCN), + Terminal(mrid="l_671_680_t2", phases=PhaseCode.ABCN)) +l_671_680 = AcLineSegment( + mrid="l_671_680", + length=1000 * METRES_PER_FOOT, + terminals=[l_671_680_t1, l_671_680_t2], + base_voltage=mv +) + +sw_671_692_t1, sw_671_692_t2 = (Terminal(mrid="sw_671_692_t1", phases=PhaseCode.ABCN), + Terminal(mrid="sw_671_692_t2", phases=PhaseCode.ABCN)) +sw_671_692 = Disconnector( + mrid="sw_671_692", + terminals=[sw_671_692_t1, sw_671_692_t2], + base_voltage=mv +) + +l_684_611_t1, l_684_611_t2 = (Terminal(mrid="l_684_611_t1", phases=PhaseCode.CN), + Terminal(mrid="l_684_611_t2", phases=PhaseCode.CN)) +l_684_611 = AcLineSegment( + mrid="l_684_611", + length=300 * METRES_PER_FOOT, + terminals=[l_684_611_t1, l_684_611_t2], + base_voltage=mv +) + +l_692_675_t1, l_692_675_t2 = (Terminal(mrid="l_692_675_t1", phases=PhaseCode.ABCN), + Terminal(mrid="l_692_675_t2", phases=PhaseCode.ABCN)) +l_692_675 = AcLineSegment( + mrid="l_692_675", + length=500 * METRES_PER_FOOT, + terminals=[l_692_675_t1, l_692_675_t2], + base_voltage=mv +) ec_634_t = Terminal(mrid="ec_634_t", phases=PhaseCode.ABCN) ec_634_pha = EnergyConsumerPhase(mrid="ec_634_pha", phase=SinglePhaseKind.A, p_fixed=160000, q_fixed=110000) ec_634_phb = EnergyConsumerPhase(mrid="ec_634_phb", phase=SinglePhaseKind.B, p_fixed=120000, q_fixed=90000) ec_634_phc = EnergyConsumerPhase(mrid="ec_634_phc", phase=SinglePhaseKind.C, p_fixed=120000, q_fixed=90000) -ec_634 = EnergyConsumer(mrid="ec_634", terminals=[ec_634_t], energy_consumer_phases=[ec_634_pha, ec_634_phb, ec_634_phc], base_voltage=lv) +ec_634 = EnergyConsumer( + mrid="ec_634", + terminals=[ec_634_t], + energy_consumer_phases=[ec_634_pha, ec_634_phb, ec_634_phc], + base_voltage=lv +) ec_645_t = Terminal(mrid="ec_645_t", phases=PhaseCode.ABCN) ec_645_pha = EnergyConsumerPhase(mrid="ec_645_pha", phase=SinglePhaseKind.A, p_fixed=0, q_fixed=0) ec_645_phb = EnergyConsumerPhase(mrid="ec_645_phb", phase=SinglePhaseKind.B, p_fixed=170000, q_fixed=125000) ec_645_phc = EnergyConsumerPhase(mrid="ec_645_phc", phase=SinglePhaseKind.C, p_fixed=0, q_fixed=0) -ec_645 = EnergyConsumer(mrid="ec_645", terminals=[ec_645_t], energy_consumer_phases=[ec_645_pha, ec_645_phb, ec_645_phc], base_voltage=mv) +ec_645 = EnergyConsumer( + mrid="ec_645", + terminals=[ec_645_t], + energy_consumer_phases=[ec_645_pha, ec_645_phb, ec_645_phc], + base_voltage=mv +) ec_646_t = Terminal(mrid="ec_646_t", phases=PhaseCode.ABC) ec_646_pha = EnergyConsumerPhase(mrid="ec_646_pha", phase=SinglePhaseKind.A, p=0, q=0) ec_646_phb = EnergyConsumerPhase(mrid="ec_646_phb", phase=SinglePhaseKind.B, p=230000, q=132000) ec_646_phc = EnergyConsumerPhase(mrid="ec_646_phc", phase=SinglePhaseKind.C, p=0, q=0) -ec_646 = EnergyConsumer(mrid="ec_646", terminals=[ec_646_t], energy_consumer_phases=[ec_646_pha, ec_646_phb, ec_646_phc], base_voltage=mv) +ec_646 = EnergyConsumer( + mrid="ec_646", + terminals=[ec_646_t], + energy_consumer_phases=[ec_646_pha, ec_646_phb, ec_646_phc], + base_voltage=mv +) ec_652_t = Terminal(mrid="ec_652_t", phases=PhaseCode.ABCN) ec_652_pha = EnergyConsumerPhase(mrid="ec_652_pha", phase=SinglePhaseKind.A, p=128000, q=86000) ec_652_phb = EnergyConsumerPhase(mrid="ec_652_phb", phase=SinglePhaseKind.B, p=0, q=0) ec_652_phc = EnergyConsumerPhase(mrid="ec_652_phc", phase=SinglePhaseKind.C, p=0, q=0) -ec_652 = EnergyConsumer(mrid="ec_652", terminals=[ec_652_t], energy_consumer_phases=[ec_652_pha, ec_652_phb, ec_652_phc], base_voltage=mv) +ec_652 = EnergyConsumer( + mrid="ec_652", + terminals=[ec_652_t], + energy_consumer_phases=[ec_652_pha, ec_652_phb, ec_652_phc], + base_voltage=mv +) ec_671_t = Terminal(mrid="ec_671_t", phases=PhaseCode.ABC) ec_671_pha = EnergyConsumerPhase(mrid="ec_671_pha", phase=SinglePhaseKind.A, p_fixed=385000, q_fixed=220000) ec_671_phb = EnergyConsumerPhase(mrid="ec_671_phb", phase=SinglePhaseKind.B, p_fixed=385000, q_fixed=220000) ec_671_phc = EnergyConsumerPhase(mrid="ec_671_phc", phase=SinglePhaseKind.C, p_fixed=385000, q_fixed=220000) -ec_671 = EnergyConsumer(mrid="ec_671", terminals=[ec_671_t], energy_consumer_phases=[ec_671_pha, ec_671_phb, ec_671_phc], base_voltage=mv) +ec_671 = EnergyConsumer( + mrid="ec_671", + terminals=[ec_671_t], + energy_consumer_phases=[ec_671_pha, ec_671_phb, ec_671_phc], + base_voltage=mv +) ec_675_t = Terminal(mrid="ec_675_t", phases=PhaseCode.ABCN) ec_675_pha = EnergyConsumerPhase(mrid="ec_675_pha", phase=SinglePhaseKind.A, p_fixed=485000, q_fixed=190000) ec_675_phb = EnergyConsumerPhase(mrid="ec_675_phb", phase=SinglePhaseKind.B, p_fixed=68000, q_fixed=60000) ec_675_phc = EnergyConsumerPhase(mrid="ec_675_phc", phase=SinglePhaseKind.C, p_fixed=290000, q_fixed=212000) -ec_675 = EnergyConsumer(mrid="ec_675", terminals=[ec_675_t], energy_consumer_phases=[ec_675_pha, ec_675_phb, ec_675_phc], base_voltage=mv) +ec_675 = EnergyConsumer( + mrid="ec_675", + terminals=[ec_675_t], + energy_consumer_phases=[ec_675_pha, ec_675_phb, ec_675_phc], + base_voltage=mv +) ec_692_t = Terminal(mrid="ec_692_t", phases=PhaseCode.ABC) ec_692_pha = EnergyConsumerPhase(mrid="ec_692_pha", phase=SinglePhaseKind.A, p=0, q=0) ec_692_phb = EnergyConsumerPhase(mrid="ec_692_phb", phase=SinglePhaseKind.B, p=0, q=0) ec_692_phc = EnergyConsumerPhase(mrid="ec_692_phc", phase=SinglePhaseKind.C, p=170000, q=151000) -ec_692 = EnergyConsumer(mrid="ec_692", terminals=[ec_692_t], energy_consumer_phases=[ec_692_pha, ec_692_phb, ec_692_phc], base_voltage=mv) +ec_692 = EnergyConsumer( + mrid="ec_692", + terminals=[ec_692_t], + energy_consumer_phases=[ec_692_pha, ec_692_phb, ec_692_phc], + base_voltage=mv +) ec_611_t = Terminal(mrid="ec_611_t", phases=PhaseCode.ABCN) ec_611_pha = EnergyConsumerPhase(mrid="ec_611_pha", phase=SinglePhaseKind.A, p=0, q=0) ec_611_phb = EnergyConsumerPhase(mrid="ec_611_phb", phase=SinglePhaseKind.B, p=0, q=0) ec_611_phc = EnergyConsumerPhase(mrid="ec_611_phc", phase=SinglePhaseKind.C, p=170000, q=80000) -ec_611 = EnergyConsumer(mrid="ec_611", terminals=[ec_611_t], energy_consumer_phases=[ec_611_pha, ec_611_phb, ec_611_phc], base_voltage=mv) +ec_611 = EnergyConsumer( + mrid="ec_611", + terminals=[ec_611_t], + energy_consumer_phases=[ec_611_pha, ec_611_phb, ec_611_phc], + base_voltage=mv +) # Distributed load on line 632-671 is unmodelled. lsc_675_t= Terminal(mrid="lsc_675_t1", phases=PhaseCode.ABCN) -lsc_675_info = ShuntCompensatorInfo(mrid="lsc_675_info", rated_voltage=4160, rated_current=48.077, rated_reactive_power=200000) +lsc_675_info = ShuntCompensatorInfo( + mrid="lsc_675_info", + rated_voltage=4160, + rated_current=48.077, + rated_reactive_power=200000 +) lsc_675 = LinearShuntCompensator(mrid="lsc_675", terminals=[lsc_675_t], asset_info=lsc_675_info) lsc_611_t = Terminal(mrid="lsc_611_t1", phases=PhaseCode.CN) -lsc_611_info = ShuntCompensatorInfo(mrid="lsc_611_info", rated_voltage=4160, rated_current=24.048, rated_reactive_power=100000) +lsc_611_info = ShuntCompensatorInfo( + mrid="lsc_611_info", + rated_voltage=4160, + rated_current=24.048, + rated_reactive_power=100000 +) lsc_611 = LinearShuntCompensator(mrid="lsc_611", terminals=[lsc_611_t], asset_info=lsc_611_info) ########################### diff --git a/src/zepben/examples/isolation_equipment_between_nodes.py b/src/zepben/examples/isolation_equipment_between_nodes.py new file mode 100644 index 0000000..904bbfb --- /dev/null +++ b/src/zepben/examples/isolation_equipment_between_nodes.py @@ -0,0 +1,76 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +""" +Example trace showing method to traverse between any 2 given `IdentifiableObject` and build a list +of `ProtectedSwitch` objects found, if any +""" + +import asyncio +import json +from typing import Tuple, Type + +from zepben.evolve import ( + NetworkStateOperators, NetworkTraceActionType, NetworkTraceStep, StepContext, Tracing, + NetworkConsumerClient, ProtectedSwitch, Recloser, LoadBreakSwitch, connect_with_token +) +from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS + + +async def main(mrids: Tuple[str, str], io_type: Type[ProtectedSwitch], feeder_mrid): + with open("config.json") as f: + c = json.loads(f.read()) + + channel = connect_with_token( + host=c["host"], + access_token=c["access_token"], + rpc_port=c["rpc_port"] + ) + client = NetworkConsumerClient(channel) + await client.get_equipment_container( + feeder_mrid, + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) + network = client.service + + nodes = [network.get(_id) for _id in mrids] + + state_operators = NetworkStateOperators.NORMAL + + found_switch = set() + found_node = [] + + def stop_condition(step: NetworkTraceStep, context: StepContext): + """if we encounter any of the equipment we have specified, we stop the trace and mark the `found_switch` list as valid""" + if step.path.to_equipment in nodes: + found_node.append(True) + return True + + def step_action(step: NetworkTraceStep, context: StepContext): + """Add any equipment matching the type passed in to the list, this list is invalid unless we trace onto our other node""" + if isinstance(step.path.to_equipment, io_type): + found_switch.add(step.path.to_equipment) + + trace = ( + Tracing.network_trace( + network_state_operators=state_operators, + action_step_type=NetworkTraceActionType.ALL_STEPS + ).add_condition(state_operators.upstream()) + .add_stop_condition(stop_condition) + .add_step_action(step_action) + ) + + queue = iter(nodes) + while not found_node: # run an upstream trace for every node specified until we encounter another specified node + await trace.run(start=next(queue), can_stop_on_start_item=False) + + all(map(print, found_switch)) # print the list of switches + print(bool(found_switch)) # print whether we found what we were looking for + + +if __name__ == "__main__": + asyncio.run(main(mrids=('50735858', '66598892'), io_type=LoadBreakSwitch, feeder_mrid='RW1292')) + asyncio.run(main(mrids=('50735858', '50295424'), io_type=Recloser, feeder_mrid='RW1292')) diff --git a/src/zepben/examples/list_ewb_network_models.py b/src/zepben/examples/list_ewb_network_models.py index de0e539..c4fa7f9 100644 --- a/src/zepben/examples/list_ewb_network_models.py +++ b/src/zepben/examples/list_ewb_network_models.py @@ -1,4 +1,4 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -27,8 +27,8 @@ new_dir.joinpath(f"{to_create}-customers.sqlite").touch() else: data_path = Path("") - network_list = None - customer_list = None + network_list = [] + customer_list = [] # Initialize EwbDataFilePaths with the EWB data directory ewb_data = EwbDataFilePaths(data_path) @@ -41,10 +41,10 @@ # Find the first date for which exists a customer database before 2011-09-10 closest_date_before = ewb_data.find_closest(DatabaseType.CUSTOMER, target_date=date(2011, 9, 10)) -print(f"\nThe last customer database before 2011-09-10: {closest_date_before.isoformat() if closest_date_before is not None else closest_date_before}") +print(f"\nThe last customer database before 2011-09-10: {closest_date_before.isoformat() or closest_date_before}") if create_temp_files: - if network_list is not None and customer_list is not None: + if network_list and customer_list: for to_cleanup in network_list: date_dir = data_path.joinpath(to_cleanup) date_dir.joinpath(f"{to_cleanup}-network-model.sqlite").unlink() @@ -58,4 +58,3 @@ print("\nTemporary files successfully cleaned up.") else: print("\nUnexpected issue while attempting to cleanup temporary files.") - diff --git a/src/zepben/examples/network_service_interactions.py b/src/zepben/examples/network_service_interactions.py index 5a97219..a7aeea7 100644 --- a/src/zepben/examples/network_service_interactions.py +++ b/src/zepben/examples/network_service_interactions.py @@ -1,21 +1,20 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from zepben.evolve import NetworkService, AcLineSegment, PerLengthSequenceImpedance, Switch, Breaker, ConductingEquipment, NameType, Meter, EnergySource, \ - Terminal - -# A `NetworkService` is a mutable node breaker network model that implements a subset of IEC61968 and IEC61970 CIM classes. -# It is essentially a collection of `IdentifiedObject`s, and they may be added and removed as desired. from zepben.evolve.services.common.resolver import per_length_impedance +from zepben.evolve import ( + NetworkService, AcLineSegment, PerLengthSequenceImpedance, Switch, Breaker, + ConductingEquipment, NameType, Meter, EnergySource, Terminal +) + +# A `NetworkService` is a mutable node breaker network model that implements a subset of +# IEC61968 and IEC61970 CIM classes. It is essentially a collection of `IdentifiedObject`s, +# and they may be added and removed as desired. network = NetworkService() -print(""" -################## -# ADDING OBJECTS # -################## -""") +print("\n##################\n# ADDING OBJECTS #\n##################\n") # We start by adding a line segment and a breaker to the network model. line = AcLineSegment(mrid="acls_123") breaker = Breaker(mrid="b_456") @@ -28,11 +27,7 @@ print(f"{invalid} added? {network.add(invalid)}") -print(""" -#################### -# QUERYING OBJECTS # -#################### -""") +print("\n####################\n# QUERYING OBJECTS #\n\n####################\n") # Use the `get` method to query the network model for an object with the specified mRID. print(f"Identified object with mrid acls_123: {network.get('acls_123')}") print(f"Identified object with mrid b_456: {network.get('b_456')}") @@ -43,7 +38,8 @@ except KeyError as error: print(error) -# Narrow the desired type with the second parameter. In makes the intent clearer, and lets IDEs lint and autocomplete according to the requested type. +# Narrow the desired type with the second parameter. In makes the intent clearer, and +# lets IDEs lint and autocomplete according to the requested type. print(f"Switch with mrid b_456 is open? {network.get('b_456', Switch).is_open()}") # A `TypeError` is raised if the object exists in the network model, but is not the correct type. @@ -52,33 +48,29 @@ except TypeError as error: print(error) -print(""" -################## -# QUERYING TYPES # -################## -""") +print("\n##################\n# QUERYING TYPES #\n##################\n") # You may use the `objects` method to iterate over all objects that inherit a specified type. -# Because the breaker is the only object in the network model that inherits from the `Switch` class, this will print "Switch: Breaker{b_456}". +# Because the breaker is the only object in the network model that inherits from the `Switch` +# class, this will print "Switch: Breaker{b_456}". for switch in network.objects(Switch): print(f"Switch: {switch}") # However, both the line and the breaker inherit from `ConductingEquipment`. -# The following line prints "Conducting equipment: AcLineSegment{acls_123}" and "Conducting equipment: Breaker{b_456}". +# The following line prints "Conducting equipment: AcLineSegment{acls_123}" +# and "Conducting equipment: Breaker{b_456}". for conducting_equipment in network.objects(ConductingEquipment): print(f"Conducting equipment: {conducting_equipment}") -# Remark: Objects generated by network.objects(BaseType) are ordered by the name of their leaf class, so all `AcLineSegment`s will appear before all `Breaker`s. +# Remark: Objects generated by network.objects(BaseType) are ordered by the name of their leaf +# class, so all `AcLineSegment`s will appear before all `Breaker`s. # The `len_of` method returns the number of objects that inherit a specified type. print(f"Number of switches: {network.len_of(Switch)}") print(f"Number of conducting equipment: {network.len_of(ConductingEquipment)}") -print(""" -############# -# RESOLVERS # -############# -""") -# There may be times when you need to reconstruct a network model from an unordered collection of identified objects. -# `NetworkService` allows you to add reference resolvers, which complete associations when the remaining object in an association is added. +print("\n#############\n# RESOLVERS #\n#############\n") +# There may be times when you need to reconstruct a network model from an unordered collection +# of identified objects. `NetworkService` allows you to add reference resolvers, which complete +# associations when the remaining object in an association is added. network.resolve_or_defer_reference(per_length_impedance(line), "plsi_789") print(f"Network has unresolved references? {network.has_unresolved_references()}") @@ -94,14 +86,11 @@ print(f"Total unresolved references: {network.num_unresolved_references()}") print(f"PerLengthSequenceImpedance of acls_123: {line.per_length_sequence_impedance}") -print(""" -######################## -# CONNECTING TERMINALS # -######################## -""") +print("\n########################\n# CONNECTING TERMINALS #\n########################\n") # Terminals in a `NetworkService` may be connected using the `connect_terminals` method. -# This automatically creates a connectivity node between the terminals, unless one of the terminals is already assigned to one. -t1, t2, t3 = Terminal(mrid="t1"), Terminal(mrid="t2"), Terminal(mrid="t3") +# This automatically creates a connectivity node between the terminals, unless one of the +# terminals is already assigned to one. +t1, t2, t3 = (Terminal(mrid=f"t{i+1}") for i in range(3)) network.add(t1) network.add(t2) network.add(t3) @@ -118,13 +107,10 @@ print(f"\t{terminal}") -print(""" -######### -# NAMES # -######### -""") -# Apart from identified objects, a `NetworkService` also supports names. Each identified object has exactly one mRID, but can have any number of names. -# Each name has a name type. In this example, we add two names of type "NMI" to the network model. +print("\n#########\n# NAMES #\n#########\n") +# Apart from identified objects, a `NetworkService` also supports names. Each identified object has +# exactly one mRID, but can have any number of names. Each name has a name type. In this example, +# we add two names of type "NMI" to the network model. meter1 = Meter() meter2 = Meter() @@ -140,11 +126,7 @@ # Remark: In practice, NMI names are not assigned to lines and breakers. -print(""" -#################### -# REMOVING OBJECTS # -#################### -""") +print("\n####################\n# REMOVING OBJECTS #\n####################\n") # You may use the `remove` method to remove objects from the network model. network.remove(line) diff --git a/src/zepben/examples/request_power_factory_models.py b/src/zepben/examples/request_power_factory_models.py index 54d2f77..faac08b 100644 --- a/src/zepben/examples/request_power_factory_models.py +++ b/src/zepben/examples/request_power_factory_models.py @@ -1,11 +1,11 @@ -# Copyright 2023 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import os -from typing import List +from typing import List, Generator import requests from gql import gql, Client @@ -16,8 +16,9 @@ # then create a Powerfactory model by selecting components of the hierarchy to use. # To use, populate the below variables with your desired targets plus the server and auth settings. -# Set of mRID/names of targets, leaving any target blank will exclude that level of hierarchy if it's the highest level -# Names are visible through the hierarchy viewer in the UI - or you can do a getNetworkHierarchy GraphQL query as per the below. +# Set of mRID/names of targets, leaving any target blank will exclude that level of hierarchy if +# it's the highest level. Names are visible through the hierarchy viewer in the UI - or you can do +# a getNetworkHierarchy GraphQL query as per the below. target_zone_substation = {"zonesub-mRID-or-name"} target_feeder = {"feeder-mRID-or-name"} target_lv = {"lvfeeder-mRID-or-name"} @@ -40,7 +41,8 @@ password = 'password' ### EXAMPLE QUERY ONLY ### -# This is an example GraphQL query for the full network hierarchy. This is not used as part of this code, and is purely illustrative. +# This is an example GraphQL query for the full network hierarchy. This is not used as part of this +# code, and is purely illustrative. # See below functions for actual queries used. ''' query network { @@ -106,109 +108,55 @@ ''' -def request_pf_model_for_a_zone_with_hv_lv(): - # Set up auth - target = [] - # Request for ZoneSub -> Feeder -> lvFeeder - body = gql(''' - query network { - getNetworkHierarchy { - substations { - mRID - name - feeders { - mRID - name - normalEnergizedLvFeeders { - mRID - name - } - } - } - } - } - ''') +def request_pf_model_for_zone(graphql_body): + """ + Request model for ZoneSub -> Feeder -> lvFeeder + """ if check_if_currently_generating_a_model(): - result = retrieve_network_hierarchy(body) - target = get_target(target, result) + result = network_client.execute(graphql_body) # retrieve network hierarchy + target = list(get_target(result)) model_id = request_pf_model(target, file_name, feeder_max_demand) - print("Power factory model creation requested, model id: " + model_id) + print(f"Power factory model creation requested, model id: {model_id}") else: print("Warning: Still generating previous model, current model will not be generated.") -def request_pf_model_for_a_zone_with_hv_only(): - target = [] - # Request for ZoneSub -> Feeder - body = gql(''' - query network { - getNetworkHierarchy { - substations { - mRID - name - feeders { - mRID - name - } - } - } - } - ''') - if check_if_currently_generating_a_model(): - result = retrieve_network_hierarchy(body) - target = get_target(target, result) - model_id = request_pf_model(target, file_name, feeder_max_demand) - print("Power factory model creation requested, model id: " + model_id) +def get_target(result) -> Generator[str, None, None]: + if target_zone_substation: + for zone_sub in (x for x in result['getNetworkHierarchy']["substations"] if + x['mRID'] in target_zone_substation or x['name'] in target_zone_substation): + yield zone_sub['mRID'] + yield from get_feeder(zone_sub) else: - print("Warning: Still generating previous model, current model will not be generated.") - - -def retrieve_network_hierarchy(body): - return network_client.execute(body) - - -def get_target(target, result): - if len(target_zone_substation) == 0: # No Zone sub was specified thus no zone sub will be added to target for zone_sub in result['getNetworkHierarchy']["substations"]: - target = get_feeder(target, zone_sub) - else: - queried_zone_sub = [x for x in result['getNetworkHierarchy']["substations"] if - x['mRID'] in target_zone_substation or x['name'] in target_zone_substation] - for zone_sub in queried_zone_sub: - target.append(zone_sub['mRID']) - target = get_feeder(target, zone_sub) - return target + yield from get_feeder(zone_sub) -def get_feeder(target, zone_sub): +def get_feeder(zone_sub) -> Generator[str, None, None]: if 'feeders' in zone_sub.keys(): - if len(target_feeder) == 0: - for feeder in zone_sub['feeders']: - if len(target_zone_substation) != 0: - target.append(feeder['mRID']) - target = get_lvfeeder(target, feeder) + if target_feeder: # Path to include only specific feeders + for feeder in (x for x in zone_sub['feeders'] if x['mRID'] in target_feeder or x['name'] in target_feeder): + yield feeder['mRID'] + yield from get_lvfeeder(feeder) else: - queried_feeder = [x for x in zone_sub['feeders'] if x['mRID'] in target_feeder or x['name'] in target_feeder] - for feeder in queried_feeder: - target.append(feeder['mRID']) - target = get_lvfeeder(target, feeder) - return target + for feeder in zone_sub['feeders']: + if target_zone_substation: + yield feeder['mRID'] + yield from get_lvfeeder(feeder) -def get_lvfeeder(target, feeder): +def get_lvfeeder(feeder) -> Generator[str, None, None]: if 'normalEnergizedLvFeeders' in feeder.keys(): - # Path to include all lvFeeders - if len(target_lv) == 0: - for lv in feeder['normalEnergizedLvFeeders']: - target.append(lv['mRID']) - # Path to include only specific lvFeeders + if target_lv: + yield from ( + x['mRID'] for x in feeder['normalEnergizedLvFeeders'] + if x['mRID'] in target_lv or x['name'] in target_lv + ) else: - queried_lv = [x for x in feeder['normalEnergizedLvFeeders'] if x['mRID'] in target_lv or x['name'] in target_lv] - for lv in queried_lv: - target.append(lv['mRID']) - return target + # Path to include all lvFeeders + yield from (lv['mRID'] for lv in feeder['normalEnergizedFeeders']) def request_pf_model(equipment_container_list: List[str], filename: str, spread_max_demand: bool = False): @@ -217,7 +165,8 @@ def request_pf_model(equipment_container_list: List[str], filename: str, spread_ :param equipment_container_list: List of EquipmentContainer mRIDs to include in the Powerfactory model. :param filename: Desired PFD filename - :param spread_max_demand: Whether to spread max demand load across transformers/loads. False will instead configure the timeseries database. + :param spread_max_demand: Whether to spread max demand load across transformers/loads. False will instead + configure the timeseries database. """ # Set isPublic to false if you only want the specific user to see the model body = gql(''' @@ -225,18 +174,22 @@ def request_pf_model(equipment_container_list: List[str], filename: str, spread_ createPowerFactoryModel(input: $input) } ''') - variables = {'input': { - 'name': filename, - 'generationSpec': {'equipmentContainerMrids': equipment_container_list, - 'distributionTransformerConfig': { - 'rGround': 0.01, - 'xGround': 0.01 - }, - 'loadConfig': { - 'spreadMaxDemand': spread_max_demand - } - }, - 'isPublic': 'true'}} + variables=dict( + input=dict( + name=filename, + generationSpec=dict( + equipmentContainerMrids=equipment_container_list, + distributionTransformerConfig=dict( + rGround=0.01, + xGround=0.01 + ), + loadConfig=dict( + spreadMaxDemand=spread_max_demand + ) + ), + isPublic='true' + ) + ) result = api_client.execute(body, variable_values=variables) return result['createPowerFactoryModel'] @@ -267,16 +220,16 @@ def check_if_currently_generating_a_model(): } } ''') - variables = { - "limit": 10, - "offset": 0, - "filter": {} - } + variables = dict( + limit=10, + offset=0, + filter={} + ) result = api_client.execute(body, variable_values=variables) - for entry in result['pagedPowerFactoryModels']['powerFactoryModels']: - if entry['state'] == 'CREATION': - return False - return True + return not any( + entry for entry in result['pagedPowerFactoryModels']['powerFactoryModels'] + if entry['state'] == 'CREATION' + ) def download_model(model_number): @@ -301,27 +254,68 @@ def download_model(model_number): } } ''') - variables = { - "modelId": model_number, - } + variables = dict( + modelId=model_number + ) result = api_client.execute(body, variable_values=variables) model_status = result['powerFactoryModelById']['state'] - if model_status == "COMPLETED": - model = requests.get(model_url, headers={'Authorization': tft}) - open(os.path.join(output_dir, file_name) + ".pfd", 'wb').write(model.content) - print(file_name + ".pfd saved at " + output_dir) - elif model_status == "CREATION": - print("Model is still being created, please download at a later time") - elif model_status == "FAILED": - print("Model creation error: " + str(result['powerFactoryModelById']['errors'])) + match model_status: + case 'COMPLETED': + model = requests.get(model_url, headers={'Authorization': tft}) + open(os.path.join(output_dir, file_name) + ".pfd", 'wb').write(model.content) + print(file_name + ".pfd saved at " + output_dir) + case "CREATION": + print("Model is still being created, please download at a later time") + case "FAILED": + print("Model creation error: " + str(result['powerFactoryModelById']['errors'])) + + +graphql_queries = dict( + zone_with_hv_lv=gql( + ''' + query network { + getNetworkHierarchy { + substations { + mRID + name + feeders { + mRID + name + normalEnergizedLvFeeders { + mRID + name + } + } + } + } + } + ''' + ), + zone_with_hv_only=gql( + ''' + query network { + getNetworkHierarchy { + substations { + mRID + name + feeders { + mRID + name + } + } + } + } + ''' + ) +) if __name__ == "__main__": # Generate model with lv - request_pf_model_for_a_zone_with_hv_lv() + request_pf_model_for_zone(graphql_queries['zone_with_hv_lv']) # Generate model without lv - request_pf_model_for_a_zone_with_hv_only() + request_pf_model_for_zone(graphql_queries['zone_with_hv_only']) # Download a model via model number download_model(123) diff --git a/src/zepben/examples/tracing.py b/src/zepben/examples/tracing.py index 2ee1d1b..fc30213 100644 --- a/src/zepben/examples/tracing.py +++ b/src/zepben/examples/tracing.py @@ -1,4 +1,4 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -8,10 +8,7 @@ # The Evolve SDK contains several factory functions for traversals that cover common use cases. import asyncio -from zepben.evolve import Switch, ConnectivityResult, connected_equipment, SinglePhaseKind, PhaseCode, \ - Feeder, LvFeeder, Terminal, AcLineSegment, FeederDirection, Breaker, Tracing, NetworkStateOperators, NetworkTraceStep, StepContext -from zepben.evolve.services.network.tracing.networktrace.actions.equipment_tree_builder import EquipmentTreeBuilder -from zepben.evolve.services.network.tracing.networktrace.actions.tree_node import TreeNode +from zepben.evolve import NetworkTraceStep, StepContext, Breaker, Switch, Feeder, LvFeeder, NetworkStateOperators # For the purposes of this example, we will use the IEEE 13 node feeder. from zepben.examples.ieee_13_node_test_feeder import network @@ -21,248 +18,301 @@ hv_feeder = network.get("hv_fdr", Feeder) lv_feeder = network.get("lv_fdr", LvFeeder) -NORMAL = NetworkStateOperators.NORMAL -CURRENT = NetworkStateOperators.CURRENT - def reset_switch(): switch.set_normally_open(False) switch.set_open(False) - print("Switch reset (normally and currently closed)") - print() + print("Switch reset (normally and currently closed)\n") def print_heading(heading): print("+" + "-" * (len(heading) + 2) + "+") print(f"| {heading} |") - print("+" + "-" * (len(heading) + 2) + "+") - print() + print("+" + "-" * (len(heading) + 2) + "+\n") -async def equipment_traces(): - # Equipment traces iterate over equipment connected in a network. - print_heading("EQUIPMENT TRACING") +async def network_trace(): + """ + Explanation of :class:`NetworkTrace` and its configurable options. - # noinspection PyArgumentList + More information about the constructors used in this function can be found in the docstring + of :class:`NetworkTrace` + """ start_item = feeder_head - visited = set() - async def print_step(ces: NetworkTraceStep, ctx: StepContext): - visited.add(ces.path.to_equipment) - print(f"\tDepth {ctx.step_number:02d}: {ces.path.to_equipment}") - - # The connected equipment trace iterates through all connected equipment depth-first, and even through open switches. - # Equipment will be revisited if a shorter path from the starting equipment is found. - print("Connected Equipment Trace:") - await Tracing.network_trace(network_state_operators=NORMAL).add_step_action(print_step).run(start_item) - await Tracing.network_trace(network_state_operators=CURRENT).add_step_action(print_step).run(start_item) - print(f"Number of equipment visited: {len(visited)}") - print() - visited.clear() - - # There is also a breadth-first version, which guarantees that each equipment is visited at most once. - print("Connected Equipment Breadth Trace:") - await connected_equipment_breadth_trace().add_step_action(print_step).run(start_item) - print(f"Number of equipment visited: {len(visited)}") - print() - visited.clear() - - # The normal connected equipment trace iterates through all equipment connected to the starting equipment in the network's normal state. - # By setting the switch from node 671 to 692 to normally open on at least one phase, the traversal will not trace through the switch. - # Even if a switch has closed phases, it will not be traced through if one or more of its phases are closed in the network's normal state. - network.get("sw_671_692", Switch).set_normally_open(True, phase=SinglePhaseKind.A) - print("Switch set to normally open on phase A") - print() - print("Normal Connected Equipment Trace:") - await normal_connected_equipment_trace().add_step_action(print_step).run(start_item) - print(f"Number of equipment visited: {len(visited)}") - print() - visited.clear() - - # The normal connected equipment trace iterates through all equipment connected to the starting equipment in the network's current state. - # By setting the switch from node 671 to 692 to currently open on at least one phase, the traversal will not trace through the switch. - # Even if a switch has closed phases, it will not be traced through if one or more of its phases are closed in the network's current state. - switch.set_open(True, phase=SinglePhaseKind.B) - print("Switch set to currently open on phase B") - print() - print("Current Connected Equipment Trace:") - await current_connected_equipment_trace().add_step_action(print_step).run(start_item) - print(f"Number of equipment visited: {len(visited)}") - print() - visited.clear() - - reset_switch() + async def network_traces(): + """ + :class:`NetworkTrace` iterates sequentially over all terminals in a network. + + By default, the trace will run: + - Depth first, stepping to any equipment marked as 'in_service'. + - Considering only the normal state of the network. + - Performing :class:`StepAction`s only once per unique equipment encountered + """ + from zepben.evolve import Tracing + + await ( + Tracing.network_trace() + ).run(start_item) + + async def network_trace_state_operators(): + """ + Both the normal and current state of the network can be operated on by passing + :class:`NetworkStateOperators` to the constructor as `network_state_operators` + """ + from zepben.evolve import Tracing, NetworkStateOperators + + await ( + Tracing.network_trace(network_state_operators=NetworkStateOperators.NORMAL) + ).run(start_item) + + await ( + Tracing.network_trace(network_state_operators=NetworkStateOperators.CURRENT) + ).run(start_item) + + + async def network_trace_queue(): + """ + :meth:`TraversalQueue.depth_first` or :meth:`TraversalQueue.breadth_first` can be passed in as + `queue` to the constructor to control which step is taken next as the `NetworkTrace` traverses + the network + """ + from zepben.evolve import Tracing, TraversalQueue + + await ( + Tracing.network_trace(queue=TraversalQueue.depth_first()) + ).run(start_item) + + await ( + Tracing.network_trace(queue=TraversalQueue.breadth_first()) + ).run(start_item) + + async def network_trace_branching(): + """ + :class:`NetworkTrace` can also be configured to run in a branching manner. This intended to be + used solely for tracing around loops both ways. + + A branching trace has the same defaults as a non_branching trace + """ + + from zepben.evolve import Tracing + await ( + Tracing.network_trace_branching() + ).run(start_item) + + # uncomment any of the following to not run them, as they have no StepActions they will do nothing + # other than silently traverse the network. + + await network_traces() + await network_trace_state_operators() + await network_trace_queue() + await network_trace_branching() + + +async def network_trace_step_actions(): + """ + Explanation of network trace :class:`StepActions` and :class:`NetworkTraceActionType` + """ + start_item = feeder_head + async def lambda_step_action(): + """ + A :class:`NetworkTrace` is useless as configured above, as we haven't specified any :class:`StepAction`s + to take as we traverse. async functions are supported as step actions To get started, let's demonstrate + a simple :class:`StepAction defined as a lambda. + """ + from zepben.evolve import Tracing + + print_heading('NetworkTrace StepAction (as lambda):') + await ( + Tracing.network_trace() + .add_step_action(lambda step, _: print(step.path)) + ).run(start_item) + + async def function_step_action(): + """ + Functions can be used if you want type hinting, or more then one line. + """ + from zepben.evolve import Tracing, NetworkTraceStep, StepContext + + print_heading('NetworkTrace StepAction (as function):') + def print_step(step: NetworkTraceStep, context: StepContext) -> None: + print(step.path) + + await ( + Tracing.network_trace() + .add_step_action(print_step) + ).run(start_item) + + async def subclassed_step_action(): + """ + And if it suits the need better, subclasses of :class:`StepAction` are also accepted, for this + approach, please read the documentation of :class:`StepAction` as there are specific methods + you will need to override. + """ + from zepben.evolve import Tracing, NetworkTraceStep, StepAction, StepContext + + print_heading('NetworkTrace StepAction (as subclass):') + class PrintingStepAction(StepAction): + def __init__(self): + super().__init__(self._apply) + + def _apply(self, step: NetworkTraceStep, context: StepContext): + print(step.path) + + await ( + Tracing.network_trace() + .add_step_action(PrintingStepAction()) + ).run(start_item) + + async def step_action_type(): + """" + With :class:`StepAction`s you may wish to only execute these for every step taken, or once + per equipment. This is configured by passing :class:`NetworkTraceActionType to the + :class:`NetworkTrace` constructor. + """ + from zepben.evolve import Tracing, NetworkTraceActionType + + print_heading('NetworkTrace (ALL_STEPS):') + await ( + Tracing.network_trace( + action_step_type=NetworkTraceActionType.ALL_STEPS + ) + .add_step_action(lambda step, _: print(step.path)) + ).run(start_item) + + print_heading('NetworkTrace (FIRST_STEP_ON_EQUIPMENT):') + await ( + Tracing.network_trace( + action_step_type=NetworkTraceActionType.FIRST_STEP_ON_EQUIPMENT + ) + .add_step_action(lambda step, _: print(step.path)) + ).run(start_item) + + # comment any of the following to skip running them + + await lambda_step_action() + await function_step_action() + await subclassed_step_action() + await step_action_type() + + +async def network_trace_conditions(): + """ + Explanation of :class:`Conditions` + """ + visited = list() -async def connectivity_traces(): - # Connectivity traces iterate over the connectivity of equipment terminals, rather than the equipment themselves. - # The tracker ensures that each equipment appears at most once as a destination in a connectivity. - print_heading("CONNECTIVITY TRACING") + start_item = feeder_head - start_item = connected_equipment(feeder_head)[0] - visited = set() + def print_step(ces: NetworkTraceStep, ctx: StepContext): + visited.append(ces.path.to_equipment) + print(f"\tDepth {ctx.step_number:02d}: {ces.path.to_equipment}") - async def print_connectivity(cr: ConnectivityResult, _: bool): - visited.add(cr) - from_phases = "".join(phase_path.from_phase.short_name for phase_path in cr.nominal_phase_paths) - to_phases = "".join(phase_path.to_phase.short_name for phase_path in cr.nominal_phase_paths) - print(f"\t{cr.from_terminal.mrid:-<15}-{from_phases:->4}-{to_phases:-<4}-{cr.to_terminal.mrid:->15}") - print("Connectivity Trace:") - await connectivity_trace().add_step_action(print_connectivity).run(start_item) - print(f"Number of connectivities visited: {len(visited)}") - print() - visited.clear() + async def conditions_stop_at_open(): + """ + As :class:`NetworkTrace` will traverse all in service connected terminals regardless of open + state, of we want to stop tracing at open switches etc. we need to add that as a condition. - # A breadth-first connectivity trace is also available. - print("Connectivity Breadth Trace:") - await connectivity_breadth_trace().add_step_action(print_connectivity).run(start_item) - print(f"Number of connectivities visited: {len(visited)}") - print() - visited.clear() + The condition is checked against the state specified with `network_state_operators` passed + to the constructor of :class:`NetworkTrace` + """ + from zepben.evolve import Tracing, stop_at_open, Switch - # The normal connectivity trace is analogous to the normal connected equipment trace, - # and likewise does not go through switches with at least one open phase. - switch.set_normally_open(True, phase=SinglePhaseKind.A) - print("Switch set to normally open on phase A") - print() - print("Normal Connectivity Trace:") - await normal_connectivity_trace().add_step_action(print_connectivity).run(start_item) - print(f"Number of connectivities visited: {len(visited)}") - print() - visited.clear() + print_heading("Network Trace Stopping at open equipment (NetworkStateOperators.NORMAL):") - switch.set_open(True, phase=SinglePhaseKind.B) - print("Switch set to currently open on phase B") - print() - print("Current Connectivity Trace:") - await current_connectivity_trace().add_step_action(print_connectivity).run(start_item) - print(f"Number of connectivities visited: {len(visited)}") - print() - visited.clear() + network.get("sw_671_692", Switch).set_normally_open(True) + print("Switch set to normally open\n") - reset_switch() + await ( + Tracing.network_trace() + .add_step_action(print_step) + .add_condition(stop_at_open()) + ).run(start_item) + print(f"Number of equipment visited: {len(visited)}") + print() -async def limited_connected_equipment_traces(): - # Limited connected equipment traces allow you to trace up to a number of steps, and optionally in a specified feeder direction. - # Running the trace returns a dictionary from each visited equipment to the number of steps away it is from a starting equipment. - # set_direction() must be run on a network before running directed traces. - print_heading("LIMITED CONNECTED EQUIPMENT TRACES") + visited.clear() + reset_switch() - switch.set_normally_open(True, phase=SinglePhaseKind.A) - print(f"Switch set to normally open on phase A.") - print() + async def conditions_downstream(): + """ + You can specify a direction to trace to achieve a directed network trace. + Tracing.set_direction() must be run on a network before performing any directed traces + """ + from zepben.evolve import Tracing, downstream, upstream - await set_direction().run(network) - print(f"Feeder direction set for each terminal.") - print() + print_heading("Downstream Network Trace:") - line = network.get("l_632_671", AcLineSegment) - normal_distances = await normal_limited_connected_equipment_trace().run([line], maximum_steps=2, feeder_direction=FeederDirection.DOWNSTREAM) - print("Normal limited connected downstream trace from line 632-671 with maximum steps of 2:") - for eq, distance in normal_distances.items(): - print(f"\tNumber of steps to {eq}: {distance}") - print(f"Number of equipment traced: {len(normal_distances)}") - print() + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) + print("Feeder direction set for each terminal.\n") - current_distances = await current_limited_connected_equipment_trace().run([line], maximum_steps=2, feeder_direction=FeederDirection.DOWNSTREAM) - print("Current limited connected downstream trace from line 632-671 with maximum steps of 2:") - for eq, distance in current_distances.items(): - print(f"\tNumber of steps to {eq}: {distance}") - print(f"Number of equipment traced: {len(current_distances)}") - print() + await ( + Tracing.network_trace() + .add_step_action(print_step) + .add_condition(downstream()) + #.add_condition(upstream()) + ).run(start_item) - remove_direction().run(network) - print(f"Feeder direction removed for each terminal.") - print() + print(f"Number of equipment visited: {len(visited)}\n") - reset_switch() + await Tracing.clear_direction().run(start_item) + visited.clear() + async def conditions_limit_equipment_steps(): + """ + Limited connected equipment traces allow you to trace up to a number of steps. + Running the trace returns a dictionary from each visited equipment to the number of steps + away it is from a starting equipment. + """ + from zepben.evolve import Tracing, downstream, limit_equipment_steps, AcLineSegment -async def phase_traces(): - # Phase traces account for which phases each terminal supports. - print_heading("PHASE TRACING") + print_heading("Downstream NetworkTrace with limited equipment steps:") - feeder_head_phase_step = phase_step.start_at(feeder_head, PhaseCode.ABCN) - switch_phase_step = phase_step.start_at(switch, PhaseCode.ABCN) - visited = set() + line = network.get("l_632_671", AcLineSegment) - async def print_phase_step(step: PhaseStep, _: bool): - visited.add(step) - phases = "" - for spk in PhaseCode.ABCN: - if spk in step.phases: - phases += spk.short_name - else: - phases += "-" - print(f'\t{step.previous and step.previous.mrid or "(START)":-<15}-{phases: ^4}-{step.conducting_equipment.mrid:->15}') + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) + print("Feeder direction set for each terminal.\n") - print("Phase Trace:") - await phase_trace().add_step_action(print_phase_step).run(feeder_head_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() + await ( + Tracing.network_trace() + .add_condition(downstream()) + .add_stop_condition(limit_equipment_steps(limit=2)) + .add_step_action(print_step) + ).run(line) - # For each normally open phase on a switch, the normal phase trace will not trace through that phase for the switch. - switch.set_normally_open(True, SinglePhaseKind.B) - print("Normal Phase Trace:") - await normal_phase_trace().add_step_action(print_phase_step).run(feeder_head_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() + await Tracing.clear_direction().run(start_item) + print(f"Feeder direction removed for each terminal.") + print() - # For each currently open phase on a switch, the current phase trace will not trace through that phase for the switch. - switch.set_open(True, SinglePhaseKind.C) - print("Current Phase Trace:") - await current_phase_trace().add_step_action(print_phase_step).run(feeder_head_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() - - # There are also directed phase traces. - # set_direction() must be run on a network before running directed traces. - # Note that set_direction() does not trace through switches with at least one open phase, - # meaning that terminals beyond such a switch are left with a feeder direction of NONE. - await Tracing.set_direction().run(network) - print(f"Feeder direction set for each terminal.") - print() + reset_switch() - print("Normal Downstream Phase Trace:") - await normal_downstream_trace().add_step_action(print_phase_step).run(feeder_head_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() + await conditions_stop_at_open() + await conditions_downstream() + await conditions_limit_equipment_steps() - print("Current Downstream Phase Trace:") - await current_downstream_trace().add_step_action(print_phase_step).run(feeder_head_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() - print("Normal Upstream Phase Trace:") - await normal_upstream_trace().add_step_action(print_phase_step).run(switch_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() +async def assigning_equipment_to_feeders(): + """ + Use :meth:`assign_equipment_to_feeders` and :meth`assign_equipment_to_lv_feeders` to assign equipment to HV and LV feeders. - print("Current Upstream Phase Trace:") - await current_upstream_trace().add_step_action(print_phase_step).run(switch_phase_step) - print(f"Number of phase steps visited: {len(visited)}") - print() - visited.clear() + :meth:`assign_equipment_to_feeders` also ensures that HV feeders that power LV feeders are associated. - Tracing.clear_direction().run(network) - print(f"Feeder direction removed for each terminal.") - print() + As with all tracing, both the normal and current state can be operated on by passing in :class:`NetworkStateOperators.NORMAL` + or :class:`NetworkStateOperators.CURRENT`. e.g. - reset_switch() + .. code-block:: python + Tracing.assign_equipment_to_feeders.run( + network_state_operators=NetworkStateOperators.NORMAL + ) + """ + from zepben.evolve import Tracing -async def assigning_equipment_to_feeders(): - # Use assign_equipment_to_feeders() and assign_equipment_to_lv_feeders() to assign equipment to HV and LV feeders. - # assign_equipment_to_feeders() also ensures that HV feeders that power LV feeders are associated. print_heading("ASSIGNING EQUIPMENT TO FEEDERS") + print(f"Equipment in HV feeder: {[eq.mrid for eq in hv_feeder.equipment]}") print(f"Equipment in LV feeder: {[eq.mrid for eq in lv_feeder.equipment]}") print(f"LV feeders powered by HV feeder: {[lvf.mrid for lvf in hv_feeder.normal_energized_lv_feeders]}") @@ -279,55 +329,111 @@ async def assigning_equipment_to_feeders(): print() -async def set_and_remove_feeder_direction(): - # Use Tracing.set_direction().run(network) to evaluate the feeder direction of each terminal. - print_heading("SETTING FEEDER DIRECTION") +async def feeder_direction(): + """ + Examples on using set/clear direction to set or clear feeder directions to or from a network. + """ + async def set_feeder_direction(): + """ + Use Tracing.set_direction().run(network) to set feeder directions to Terminals in the network. + + .. code-block:: python + + await Tracing.clear_direction().run( + network + ) + + As with all tracing, both the normal and current state can be operated on by passing in :class:`NetworkStateOperators.NORMAL` + or :class:`NetworkStateOperators.CURRENT`. e.g. + + .. code-block:: python + + await Tracing.clear_direction().run( + network, + network_state_operators=NetworkStateOperators.CURRENT + ) + """ + from zepben.evolve import Tracing, NetworkStateOperators, Terminal + + print_heading("SETTING FEEDER DIRECTION") + + consumer_terminal = network.get("ec_675_t", Terminal) + print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") + print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") + print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") + print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") + print() + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.CURRENT) + print("Normal and current feeder direction set.") + print() + print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") + print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") + print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") + print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") + print() + + async def clear_feeder_direction(): + """ + Use Tracing.clear_direction().run(network) to clear the feeder direction for Terminals in the network. + + .. code-block:: python + + await Tracing.set_direction().run( + network + ) + + As with all tracing, both the normal and current state can be operated on by passing in + :class:`NetworkStateOperators.NORMAL` or :class:`NetworkStateOperators.CURRENT`. e.g. + + .. code-block:: python + + await Tracing.set_direction().run( + network, + network_state_operators=NetworkStateOperators.CURRENT + ) + """ + from zepben.evolve import Tracing, NetworkStateOperators, Terminal + + print_heading("REMOVING FEEDER DIRECTION") + + consumer_terminal = network.get("ec_675_t", Terminal) + print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") + print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") + print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") + print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") + print() + await Tracing.clear_direction().run(consumer_terminal, network_state_operators=NetworkStateOperators.NORMAL) + await Tracing.clear_direction().run(consumer_terminal, network_state_operators=NetworkStateOperators.CURRENT) + print("Normal and current feeder direction removed.") + print() + print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") + print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") + print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") + print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") + print() + + reset_switch() + + from zepben.evolve import SinglePhaseKind + switch.set_normally_open(True, phase=SinglePhaseKind.A) print(f"Switch set to normally open on phase A. Switch is between feeder head and energy consumer 675.") - consumer_terminal = network.get("ec_675_t", Terminal) - print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") - print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") - print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") - print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") - print() - await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) - await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.CURRENT) - print("Normal and current feeder direction set.") - print() - print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") - print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") - print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") - print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") - print() - - # Use Tracing.clear_direction().run(network) to remove feeder directions. - # While Tracing.set_direction().run(network) must be awaited, remove_direction().run(network) does not, because it is not asynchronous. - print_heading("REMOVING FEEDER DIRECTION") - - consumer_terminal = network.get("ec_675_t", Terminal) - print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") - print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") - print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") - print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") - print() - Tracing.clear_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) - Tracing.clear_direction().run(network, network_state_operators=NetworkStateOperators.CURRENT) - print("Normal and current feeder direction removed.") - print() - print(f"Normal feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.normal_feeder_direction}") - print(f"Current feeder direction of HV feeder head terminal: {hv_feeder.normal_head_terminal.current_feeder_direction}") - print(f"Normal feeder direction of energy consumer 675 terminal: {consumer_terminal.normal_feeder_direction}") - print(f"Current feeder direction of energy consumer 675 terminal: {consumer_terminal.current_feeder_direction}") - print() - - reset_switch() + await set_feeder_direction() + await clear_feeder_direction() async def trees(): - # A downstream tree contains all non-intersecting equipment paths starting from a common equipment and following downstream terminals. - # The same equipment may appear multiple times in the tree if the network contains multiple downstream paths to the equipment, i.e. loops. - # Similar to connected equipment traces, either the normal or current state of the network may be used to determine whether to trace through each switch. + """ + A downstream tree contains all non-intersecting equipment paths starting from a common equipment + and following downstream terminals. The same equipment may appear multiple times in the tree if + the network contains multiple downstream paths to the equipment, i.e. loops. As this is backed by + a NetworkTrace, either the normal or current state of the network may be used to determine whether + to trace through each switch when combined with `Conditions.stop_at_open` + """ + from zepben.evolve import Tracing, SinglePhaseKind, EquipmentTreeBuilder, TreeNode, NetworkStateOperators + print_heading("DOWNSTREAM TREES") def desc_lines(node: TreeNode): @@ -336,7 +442,7 @@ def desc_lines(node: TreeNode): is_last_child = i == len(children) - 1 branch_char = "┗" if is_last_child else "┣" stem_char = " " if is_last_child else "┃" - yield f"{branch_char}━{child.conducting_equipment}" + yield f"{branch_char}━{child.identified_object}" for line in desc_lines(child): yield f"{stem_char} {line}" @@ -344,44 +450,45 @@ def print_tree(root_node: TreeNode): print(root_node.identified_object) for line in desc_lines(root_node): print(line) + print() switch.set_open(True, SinglePhaseKind.C) - print("Switch set to currently open on phase C.") - print() + print("Switch set to currently open on phase C.\n") - await Tracing.set_direction().run(network) - print("Feeder direction set.") - print() + await Tracing.set_direction().run(network, network_state_operators=NetworkStateOperators.NORMAL) + print("Feeder direction set.\n") print("Normal Downstream Tree:") equip_tree_builder = EquipmentTreeBuilder() - await Tracing.network_trace().add_step_action(equip_tree_builder).run(feeder_head) + await ( + Tracing.network_trace() + .add_step_action(equip_tree_builder) + .run(feeder_head) + ) print_tree(next(equip_tree_builder.roots)) - print() print("Current Downstream Tree:") cur_equip_tree_builder = EquipmentTreeBuilder() - await Tracing.network_trace( - network_state_operators=NetworkStateOperators.CURRENT - ).add_step_action(cur_equip_tree_builder).run(feeder_head) + await ( + Tracing.network_trace( + network_state_operators=NetworkStateOperators.CURRENT + ).add_step_action(cur_equip_tree_builder) + ).run(feeder_head) print_tree(next(cur_equip_tree_builder.roots)) - print() - Tracing.clear_direction().run(network) - print(f"Feeder direction removed for each terminal.") - print() + await Tracing.clear_direction().run(feeder_head) + print(f"Feeder direction removed for each terminal.\n") reset_switch() async def main(): # All examples are self-contained. Feel free to comment out any of the following lines to isolate specific examples. + await network_trace() + await network_trace_step_actions() + await network_trace_conditions() await assigning_equipment_to_feeders() - await set_and_remove_feeder_direction() - await equipment_traces() - await limited_connected_equipment_traces() - await connectivity_traces() - await phase_traces() + await feeder_direction() await trees() if __name__ == "__main__": diff --git a/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py b/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py index 5457a10..b767c3a 100644 --- a/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py +++ b/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py @@ -1,4 +1,4 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -8,18 +8,18 @@ import csv import json import os -from typing import Any +from typing import Any, List, Union from zepben.evolve import NetworkConsumerClient, PhaseStep, PhaseCode, AcLineSegment, \ Switch, normal_downstream_trace, FeederDirection, connect_with_token from zepben.evolve.services.network.tracing.phases.phase_step import start_at from zepben.protobuf.nc.nc_requests_pb2 import IncludedEnergizedContainers -with open("config.json") as f: - c = json.loads(f.read()) - async def main(): + with open("config.json") as f: + c = json.loads(f.read()) + print("Connecting to Server") channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) @@ -32,8 +32,7 @@ async def main(): os.makedirs("csvs", exist_ok=True) for feeder in result.feeders.values(): print(f"Fetching {feeder.mrid}") - network = await get_feeder_network(channel, feeder.mrid) - if not network: # Skip feeders that fail to pull down + if not (network := await get_feeder_network(channel, feeder.mrid)): # Skip feeders that fail to pull down print(f"Failed to retrieve feeder {feeder.mrid}") continue for io in network.objects(Switch): @@ -74,33 +73,36 @@ async def save_to_csv(data: dict[str, tuple[list[Any], bool]], feeder_mrid): async def get_feeder_network(channel, feeder_mrid): client = NetworkConsumerClient(channel) - result = (await client.get_equipment_container(mrid=feeder_mrid, include_energized_containers=IncludedEnergizedContainers.INCLUDE_ENERGIZED_LV_FEEDERS)) + result = ( + await client.get_equipment_container( + mrid=feeder_mrid, + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) + ) if result.was_failure: print(f"Failed: {result.thrown}") return None return client.service -async def get_downstream_trace(ce: ConductingEquipment, phase_code: PhaseCode) -> list[Any]: - state_operators = NetworkStateOperators.NORMAL - trace = Tracing.network_trace().add_condition(state_operators.downstream()) - l_type: [str, str, float] = [] +async def get_downstream_trace(ce: ConductingEquipment, phase_code: PhaseCode) -> list[Union[str, float]]: + l_type: List[Union[str, float]] = [] def collect_eq_in(): async def add_eq(ps: NetworkTraceStep, _): equip = ps.path.to_equipment if isinstance(equip, AcLineSegment): - l_type.append(equip.mrid) - l_type.append(equip.asset_info.name) - if equip.length is not None: - l_type.append(equip.length) - else: - l_type.append(0) + nonlocal l_type + l_type.extend((equip.mrid, equip.asset_info.name, equip.length or 0)) return add_eq - trace.add_step_action(collect_eq_in()) - await trace.run(start=ce, phases=phase_code) + await ( + Tracing.network_trace() + .add_condition(downstream()) + .add_step_action(collect_eq_in()) + ).run(start=ce, phases=phase_code) + return l_type diff --git a/src/zepben/examples/tracing_example.py b/src/zepben/examples/tracing_example.py index 3663533..6f3628b 100644 --- a/src/zepben/examples/tracing_example.py +++ b/src/zepben/examples/tracing_example.py @@ -8,15 +8,17 @@ import asyncio import json -from zepben.evolve import NetworkConsumerClient, PhaseCode, AcLineSegment, connect_with_token, EnergyConsumer, \ - PowerTransformer, ConductingEquipment, Tracing, NetworkStateOperators, NetworkTraceStep -from zepben.protobuf.nc.nc_requests_pb2 import IncludedEnergizedContainers - -with open("config.json") as f: - c = json.loads(f.read()) +from zepben.evolve import ( + NetworkConsumerClient, PhaseCode, AcLineSegment, connect_with_token, EnergyConsumer, + PowerTransformer, ConductingEquipment, Tracing, NetworkTraceStep, downstream, upstream +) +from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS async def main(): + with open("config.json") as f: + c = json.loads(f.read()) + print("Connecting to Server") channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) @@ -27,19 +29,16 @@ async def main(): for feeder in result.feeders.values(): if feeder.mrid != "WD24": continue - print() - print(f"Fetching {feeder.mrid}") + print(f"\nFetching {feeder.mrid}") network = await get_feeder_network(channel, feeder.mrid) - print() - print("Downstream Trace Example..") + print("\nDownstream Trace Example..") # Get the count of customers per transformer for io in network.objects(PowerTransformer): customers = await get_downstream_customer_count(io, PhaseCode.ABCN) print(f"Transformer {io.mrid} has {customers} Energy Consumer(s)") - print() - print("Upstream Trace Example..") + print("\nUpstream Trace Example..") for ec in network.objects(EnergyConsumer): upstream_length = await get_upstream_length(ec, PhaseCode.ABCN) print(f"Energy Consumer {ec.mrid} --> Upstream Length: {upstream_length}") @@ -47,14 +46,14 @@ async def main(): async def get_feeder_network(channel, feeder_mrid): client = NetworkConsumerClient(channel) - (await client.get_equipment_container(mrid=feeder_mrid, - include_energized_containers=IncludedEnergizedContainers.INCLUDE_ENERGIZED_LV_FEEDERS)).throw_on_error() + (await client.get_equipment_container( + mrid=feeder_mrid, + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + )).throw_on_error() return client.service async def get_downstream_customer_count(ce: ConductingEquipment, phase_code: PhaseCode) -> int: - state_operators = NetworkStateOperators.NORMAL - trace = Tracing.network_trace().add_condition(state_operators.downstream()) customer_count = 0 def collect_eq_in(): @@ -64,14 +63,16 @@ async def add_eq(ps: NetworkTraceStep, _): customer_count += 1 return add_eq - trace.add_step_action(collect_eq_in()) - await trace.run(start=ce, phases=phase_code) + await ( + Tracing.network_trace() + .add_condition(downstream()) + .add_step_action(collect_eq_in()) + ).run(start=ce, phases=phase_code) + return customer_count async def get_upstream_length(ce: ConductingEquipment, phases: PhaseCode) -> int: - state_operators = NetworkStateOperators.NORMAL - trace = Tracing.network_trace().add_condition(state_operators.upstream()) upstream_length = 0 def collect_eq_in(): @@ -83,8 +84,12 @@ async def add_eq(ps: NetworkTraceStep, _): upstream_length += equip.length return add_eq - trace.add_step_action(collect_eq_in()) - await trace.run(start=ce, phases=phases) + await ( + Tracing.network_trace() + .add_condition(upstream()) + .add_step_action(collect_eq_in()) + ).run(start=ce, phases=phases) + return upstream_length if __name__ == "__main__": diff --git a/src/zepben/examples/translating_to_pandapower_model.py b/src/zepben/examples/translating_to_pandapower_model.py index de8ea0e..6eeb481 100644 --- a/src/zepben/examples/translating_to_pandapower_model.py +++ b/src/zepben/examples/translating_to_pandapower_model.py @@ -1,4 +1,4 @@ -# Copyright 2023 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -81,13 +81,13 @@ async def main(): print() -def add_energy_source(network: NetworkService, connect_to_terminal: Terminal): +def add_energy_source(_network: NetworkService, connect_to_terminal: Terminal): bv = connect_to_terminal.conducting_equipment.base_voltage es_t = Terminal(phases=connect_to_terminal.phases) es = EnergySource(terminals=[es_t], base_voltage=bv) - network.add(es_t) - network.add(es) - network.connect_terminals(es_t, connect_to_terminal) + _network.add(es_t) + _network.add(es) + _network.connect_terminals(es_t, connect_to_terminal) if __name__ == "__main__": From 363c4b3a5bf9a5b6adad636907c03f54ddf22735 Mon Sep 17 00:00:00 2001 From: Max Chesterfield Date: Wed, 11 Jun 2025 01:14:12 +1000 Subject: [PATCH 12/12] one more i guess Signed-off-by: Max Chesterfield --- .../examples/studies/suspect_end_of_line.py | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/src/zepben/examples/studies/suspect_end_of_line.py b/src/zepben/examples/studies/suspect_end_of_line.py index 16ee69b..3cd837f 100644 --- a/src/zepben/examples/studies/suspect_end_of_line.py +++ b/src/zepben/examples/studies/suspect_end_of_line.py @@ -1,4 +1,4 @@ -# Copyright 2022 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this @@ -15,9 +15,8 @@ from zepben.eas.client.eas_client import EasClient from zepben.eas.client.study import Study, Result, GeoJsonOverlay from zepben.evolve import PowerTransformer, ConductingEquipment, EnergyConsumer, AcLineSegment, \ - NetworkConsumerClient, normal_upstream_trace, PhaseStep, PhaseCode, PowerElectronicsConnection, Feeder, PowerSystemResource, Location, \ - normal_downstream_trace, connect_with_token -from zepben.evolve.services.network.tracing.phases.phase_step import start_at + NetworkConsumerClient, PhaseCode, PowerElectronicsConnection, Feeder, PowerSystemResource, Location, \ + connect_with_token, NetworkTraceStep, Tracing, downstream, upstream from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS @@ -90,19 +89,21 @@ async def main(): def collect_eq_provider(collection: Set[ConductingEquipment]): - async def collect_equipment(ps: PhaseStep, _): - collection.add(ps.conducting_equipment) + async def collect_equipment(ps: NetworkTraceStep, _): + collection.add(ps.path.to_equipment) return collect_equipment async def get_downstream_eq(ce: ConductingEquipment) -> Set[ConductingEquipment]: - trace = normal_downstream_trace() - phase_step = start_at(ce, PhaseCode.ABCN) - equipment_set = set() - trace.add_step_action(collect_eq_provider(equipment_set)) - await trace.run(start_item=phase_step, can_stop_on_start_item=False) + + await ( + Tracing.network_trace() + .add_condition(downstream()) + .add_step_action(collect_eq_provider(equipment_set)) + ).run(start=ce, phases=PhaseCode.ABCN, can_stop_on_start_item=False) + return equipment_set @@ -110,7 +111,13 @@ async def fetch_feeder_and_trace(feeder_mrid: str, rpc_channel): print(f"Fetching Feeder {feeder_mrid}") client = NetworkConsumerClient(rpc_channel) - result = (await client.get_equipment_container(mrid=feeder_mrid, expected_class=Feeder, include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS)) + result = ( + await client.get_equipment_container( + mrid=feeder_mrid, + expected_class=Feeder, + include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS + ) + ) if result.was_failure: print(f"Failed: {result.thrown}") return {} @@ -131,15 +138,20 @@ async def fetch_feeder_and_trace(feeder_mrid: str, rpc_channel): return transformer_to_suspect_end -async def get_transformer_to_suspect_end(transformer_to_eq: Dict[str, Set[ConductingEquipment]]) -> Dict[str, Tuple[int, Set[ConductingEquipment]]]: +async def get_transformer_to_suspect_end( + transformer_to_eq: Dict[str, Set[ConductingEquipment]] +) -> Dict[str, Tuple[int, Set[ConductingEquipment]]]: + transformer_to_suspect_end: Dict[str, (int, List[ConductingEquipment])] = {} for pt_mrid, eq_list in transformer_to_eq.items(): - single_terminal_junctions = [eq for eq in eq_list if not isinstance(eq, (EnergyConsumer, PowerElectronicsConnection)) and len(list(eq.terminals)) == 1] - - upstream_eq = set() - for stj in single_terminal_junctions: - upstream_eq_up_to_pt = await _get_upstream_eq_up_to_transformer(stj) - upstream_eq.update(upstream_eq_up_to_pt) + single_terminal_junctions = [ + eq for eq in eq_list + if not isinstance(eq, (EnergyConsumer, PowerElectronicsConnection)) + and len(list(eq.terminals)) == 1 + ] + upstream_eq = set( + await _get_upstream_eq_up_to_transformer(stj) for stj in single_terminal_junctions + ) transformer_to_suspect_end[pt_mrid] = (len(single_terminal_junctions), list(upstream_eq)) @@ -155,6 +167,7 @@ async def upload_suspect_end_of_line_study( tags: List[str], styles: List ) -> None: + class_to_properties = { EnergyConsumer: { "name": lambda ec: ec.name, @@ -189,17 +202,19 @@ async def upload_suspect_end_of_line_study( async def _get_upstream_eq_up_to_transformer(ce: ConductingEquipment) -> Set[ConductingEquipment]: eqs = set() - trace = normal_upstream_trace() - phase_step = start_at(ce, PhaseCode.ABCN) - trace.add_step_action(collect_eq_provider(eqs)) - trace.add_stop_condition(_is_transformer) - await trace.run(start_item=phase_step, can_stop_on_start_item=False) + await ( + Tracing.network_trace() + .add_condition(upstream()) + .add_step_action(collect_eq_provider(eqs)) + .add_stop_condition(_is_transformer) + ).run(start=ce, phases=PhaseCode.ABCN, can_stop_on_start_item=False) + return eqs -async def _is_transformer(ps: PhaseStep): - return isinstance(ps.conducting_equipment, PowerTransformer) +async def _is_transformer(ps: NetworkTraceStep): + return isinstance(ps.path.to_equipment, PowerTransformer) def _suspect_end_count_from(pt_to_sus_end: Dict[str, Tuple[int, List[ConductingEquipment]]]): @@ -210,7 +225,11 @@ def fun(pt: PowerTransformer): return fun -def to_geojson_feature_collection(psrs: List[PowerSystemResource], class_to_properties: Dict[Type, Dict[str, Callable[[Any], Any]]]) -> FeatureCollection: +def to_geojson_feature_collection( + psrs: List[PowerSystemResource], + class_to_properties: Dict[Type, Dict[str, Callable[[Any], Any]]] +) -> FeatureCollection: + features = [] for psr in psrs: properties_map = class_to_properties.get(type(psr)) @@ -221,7 +240,11 @@ def to_geojson_feature_collection(psrs: List[PowerSystemResource], class_to_prop return FeatureCollection(features) -def to_geojson_feature(psr: PowerSystemResource, property_map: Dict[str, Callable[[PowerSystemResource], Any]]) -> Union[Feature, None]: +def to_geojson_feature( + psr: PowerSystemResource, + property_map: Dict[str, Callable[[PowerSystemResource], Any]] +) -> Union[Feature, None]: + geometry = to_geojson_geometry(psr.location) if geometry is None: return None