import math
import os
import pytest
import gridoxide
FIXTURES = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
"tests",
"data",
"pgm",
"short_circuit",
)
def fixture(name):
return os.path.join(FIXTURES, name, "input.json")
def test_three_phase_fault_is_balanced_and_positive_sequence():
result = gridoxide.short_circuit(fixture("three_phase_c_maximum"))
assert len(result.faults) == 1
fault = result.faults[0]
assert fault.id == 10
assert fault.i_f[0] == pytest.approx(fault.i_f[1], rel=1e-9)
assert fault.i_f[1] == pytest.approx(fault.i_f[2], rel=1e-9)
assert fault.i_f[0] > 1e4
for node in result.nodes:
zero, positive, negative = node.sequence
assert zero < 1e-9, f"node {node.id} zero sequence {zero}"
assert negative < 1e-9, f"node {node.id} negative sequence {negative}"
assert positive > 0.1
def test_single_phase_fault_is_unbalanced_and_grounded():
result = gridoxide.short_circuit(fixture("single_phase_to_ground_c_maximum"))
fault = result.faults[0]
assert fault.i_f[0] > 1e3
assert fault.i_f[1] == pytest.approx(0.0, abs=1e-6)
assert fault.i_f[2] == pytest.approx(0.0, abs=1e-6)
assert any(node.sequence[0] > 1e-3 for node in result.nodes)
def test_two_phase_fault_has_no_zero_sequence():
result = gridoxide.short_circuit(fixture("two_phase_c_maximum"))
fault = result.faults[0]
assert fault.i_f[0] == pytest.approx(0.0, abs=1e-6)
assert fault.i_f[1] > 1e3
assert fault.i_f[1] == pytest.approx(fault.i_f[2], rel=1e-6)
def test_minimum_scaling_gives_a_smaller_current_than_maximum():
path = fixture("three_phase_c_maximum")
big = gridoxide.short_circuit(path, scaling="max").faults[0].i_f[0]
small = gridoxide.short_circuit(path, scaling="min").faults[0].i_f[0]
assert small < big
assert small / big == pytest.approx(1.00 / 1.10, rel=1e-3)
def test_nodes_carry_document_ids_and_volts():
result = gridoxide.short_circuit(fixture("three_phase_c_maximum"))
ids = sorted(node.id for node in result.nodes)
assert ids == [0, 1, 2, 3]
for node in result.nodes:
assert node.energized is True
assert len(node.u_pu) == 3
assert len(node.u_angle) == 3
assert all(math.isfinite(v) for v in node.u)
def test_sources_report_their_contributions():
result = gridoxide.short_circuit(fixture("three_phase_c_maximum"))
ids = sorted(source.id for source in result.sources)
assert ids == [4, 5]
assert max(s.i[0] for s in result.sources) > 1e4
def test_rejects_an_unknown_scaling():
with pytest.raises(ValueError, match="max"):
gridoxide.short_circuit(fixture("three_phase_c_maximum"), scaling="sideways")
def test_reports_a_missing_file_clearly():
with pytest.raises(RuntimeError, match="reading"):
gridoxide.short_circuit(os.path.join(FIXTURES, "no-such-fixture", "input.json"))
def test_branch_currents_account_for_the_fault_current():
result = gridoxide.short_circuit(fixture("three_phase_c_maximum"))
assert {b.id for b in result.branches} == {6, 7, 8}
assert all(b.energized for b in result.branches)
line = next(b for b in result.branches if b.id == 8)
fault = result.faults[0]
for phase in range(3):
assert line.i_to[phase] == pytest.approx(fault.i_f[phase], rel=1e-9)
assert line.i_from[0] != pytest.approx(line.i_to[0], rel=1e-6)
def test_shunt_current_needs_a_zero_sequence_fault():
balanced = gridoxide.short_circuit(fixture("three_phase_c_maximum"))
grounded = gridoxide.short_circuit(fixture("single_phase_to_ground_c_maximum"))
assert [s.id for s in balanced.shunts] == [9]
assert balanced.shunts[0].i[0] == pytest.approx(0.0, abs=1e-9)
assert grounded.shunts[0].i[0] > 1.0