Network#

popo.network describes a circuit as it is defined: its lines, loads, transformers, PV systems and storage units. Solving the circuit and reading its results is covered in Power flow.

Loading a circuit#

Circuit.load clears the OpenDSS engine, compiles a master file and solves it. The returned Circuit is a view of the compiled circuit.

from pathlib import Path

from popo.network import Circuit

FEEDER = Path("tests/data/dss_uk_network_17_1_1149/master_network_17_1_1149.dss")

circuit = Circuit.load(FEEDER)
circuit.summary()
{'converged': True,
 'n_buses': 380,
 'n_lines': 378,
 'n_loads': 188,
 'n_transformers': 1,
 'total_p_loss_kw': 10.05532368243081,
 'total_q_loss_kvar': 10.047733903634096}

To build a circuit from OpenDSS commands instead of a file, create Circuit() and pass each command to command(), as the quickstart does.

Elements as records#

The circuit yields its elements as records: lines, loads, transformers, pvsystems and storage_units. Each record is a small dataclass, so a collection converts directly to a DataFrame.

lines = pd.DataFrame(circuit.lines)
lines.head()
name bus1 bus2 phases length
0 sourcez sourcebus 1149 3 1.000
1 feeder1_1_11491 1_1149_1 1_1149_171_1 3 0.001
2 line1_1149_171_55 1_1149_171_53 1_1149_171_56 3 6.710
3 line1_1149_171_101 1_1149_171_96 1_1149_171_102 3 3.364
4 line1_1149_171_103 1_1149_171_97 1_1149_171_104 3 4.033
list(circuit.transformers)
[Transformer(name='tr171_11491_1', rating_kva=500.0, low_voltage_bus='1_1149_1', secondary_kv=0.416, normal_rating_kva=550.0)]

A Transformer keeps its nameplate rating (500 kVA here) separate from its normal thermal rating (550 kVA), because a loading study needs to know which of the two it is comparing against.

For a property that no record covers, command() passes an OpenDSS command through and returns its answer:

circuit.command("? Line.line1_1149_171_55.length")
'6.7095'

Customers#

residential_loads() lists a feeder’s low-voltage residential loads as (load name, bus) pairs. Every scenario function in Scenarios takes its customers in this form.

from popo.network import residential_loads

customers = residential_loads(circuit.dss)
len(customers), customers[:3]
(188,
 [('load1_1149_171_1', '1_1149_171_102'),
  ('load1_1149_171_2', '1_1149_171_104'),
  ('load1_1149_171_3', '1_1149_171_141')])

Describing a feeder#

profile_circuit() summarises a circuit at its defined loading.

from dataclasses import asdict

from popo.network import measure_sensitivity, profile_circuit

pd.Series(asdict(profile_circuit(circuit.dss, "UK feeder")))
name                  UK feeder
customers                   188
buses                       380
lines                       378
transformers                  1
rating_kva                500.0
single_phase_share          1.0
converged                  True
min_voltage_pu            0.942
max_voltage_pu            1.033
dtype: object

measure_sensitivity() raises the loading, solves again and measures how far the lowest bus voltage moves per megawatt of substation demand. A high sensitivity means a voltage limit is likely to bind before the transformer’s thermal rating does.

pd.Series(asdict(measure_sensitivity(circuit.dss, "UK feeder")))
name                     UK feeder
base_multiplier                1.0
step_multiplier                2.0
min_voltage_base_pu          0.942
min_voltage_step_pu          0.857
active_base_kw             197.857
active_step_kw             387.504
reactive_base_kvar          71.775
sensitivity_pu_per_mw        0.446
dtype: object

Doubling the loading lowers the minimum voltage from 0.942 pu to 0.857 pu, a sensitivity of 0.45 pu per MW.

Feeders with many transformers#

A North American feeder can have hundreds of pole-mounted transformers. Recording all of them in every scenario is slow, so popo provides three steps for choosing which to monitor:

  • read_transformers() reads the transformers from an OpenDSS definition file, keeping the nameplate and normal ratings separate.

  • screen() steps the feeder through a short window and returns each transformer’s peak apparent power and voltage extremes.

  • select_by_utilisation() takes the most heavily loaded transformers from the screen, where a thermal limit can bind, and keeps that selection fixed across scenarios so a sweep compares the same assets.

Limitations#

  • OpenDSS runs one circuit per process. A Circuit is a view of whatever is compiled now, so loading another circuit changes what every existing Circuit sees.

  • Records describe the circuit as defined, at its nominal ratings. Values that change during a simulation, such as a load’s demand at a given step, are read from Power flow.

  • residential_loads identifies customers by their nominal voltage, below 0.3 kV by default. A feeder modelled with a different convention needs a different secondary_kv.