Skip to main content
Glama
test_inverter_control.py12.8 kB
""" Unit tests for inverter control functionality. """ import pytest from opendss_mcp.utils.inverter_control import ( load_curve, configure_volt_var_control, configure_volt_watt_control, list_available_curves, get_inverter_status, ) from opendss_mcp.tools.feeder_loader import load_ieee_test_feeder import opendssdirect as dss def test_load_ieee1547_curve(): """Test loading IEEE 1547 control curve.""" # Load the curve curve_points = load_curve("IEEE1547") # Verify correct number of points assert len(curve_points) == 4, "IEEE1547 curve should have 4 points" # Verify points are tuples for point in curve_points: assert isinstance(point, tuple), "Each point should be a tuple" assert len(point) == 2, "Each point should have 2 values (voltage, var)" # Verify expected values expected_points = [(0.92, 0.44), (0.98, 0.0), (1.02, 0.0), (1.08, -0.44)] assert ( curve_points == expected_points ), "IEEE1547 curve points don't match expected values" def test_load_rule21_curve(): """Test loading Rule 21 control curve.""" # Load the curve (case insensitive) curve_points = load_curve("rule21") # Verify correct number of points assert len(curve_points) == 4, "RULE21 curve should have 4 points" # Verify expected values expected_points = [(0.95, 0.44), (0.99, 0.0), (1.01, 0.0), (1.05, -0.44)] assert ( curve_points == expected_points ), "RULE21 curve points don't match expected values" def test_load_invalid_curve(): """Test loading a non-existent curve raises appropriate error.""" with pytest.raises(FileNotFoundError): load_curve("NONEXISTENT_CURVE") def test_list_available_curves(): """Test listing available standard curves.""" curves = list_available_curves() # Should have at least 2 standard curves assert len(curves) >= 2, "Should have at least IEEE1547 and RULE21" # Verify structure for curve in curves: assert "name" in curve assert "file" in curve assert "description" in curve assert "type" in curve # Verify IEEE1547 is present ieee_curve = next((c for c in curves if c["name"] == "IEEE1547"), None) assert ieee_curve is not None, "IEEE1547 should be in available curves" assert ieee_curve["type"] == "volt-var" def test_configure_volt_var(): """Test configuring volt-var control on a PV system.""" # Load IEEE13 feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"], f"Failed to load feeder: {load_result.get('errors')}" # Add a test PV system dss.Text.Command( "New PVSystem.TestPV Bus1=675 Phases=3 kV=4.16 kVA=500 Pmpp=500 irradiance=1.0" ) # Solve power flow dss.Solution.Solve() assert dss.Solution.Converged(), "Power flow should converge" # Load curve and configure volt-var control curve_points = load_curve("IEEE1547") configure_volt_var_control("TestPV", curve_points, response_time=10.0) # Verify XYCurve was created all_curves = dss.XYCurves.AllNames() assert any( "testpv" in curve.lower() for curve in all_curves ), "XYCurve should be created" # Verify power flow still solves with control dss.Solution.Solve() assert dss.Solution.Converged(), "Power flow should converge with volt-var control" def test_configure_volt_var_with_rule21(): """Test configuring volt-var control with Rule 21 curve.""" # Load IEEE13 feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Add a test PV system dss.Text.Command( "New PVSystem.TestPV2 Bus1=611 Phases=3 kV=4.16 kVA=300 Pmpp=300 irradiance=1.0" ) # Load Rule 21 curve and configure curve_points = load_curve("RULE21") configure_volt_var_control("TestPV2", curve_points, response_time=5.0) # Verify XYCurve was created all_curves = dss.XYCurves.AllNames() assert any( "testpv2" in curve.lower() for curve in all_curves ), "XYCurve should be created for TestPV2" # Solve and verify convergence dss.Solution.Solve() assert dss.Solution.Converged() def test_configure_volt_watt(): """Test configuring volt-watt control on a PV system.""" # Load IEEE13 feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Add a test PV system dss.Text.Command( "New PVSystem.TestPV3 Bus1=652 Phases=1 kV=2.4 kVA=200 Pmpp=200 irradiance=1.0" ) # Define volt-watt curve (curtail above 1.06 pu) vw_curve = [(0.0, 1.0), (1.06, 1.0), (1.10, 0.2)] # Configure volt-watt control configure_volt_watt_control("TestPV3", vw_curve) # Verify XYCurve was created all_curves = dss.XYCurves.AllNames() assert any( "testpv3" in curve.lower() for curve in all_curves ), "XYCurve should be created for volt-watt" # Solve and verify convergence dss.Solution.Solve() assert dss.Solution.Converged() def test_get_inverter_status(): """Test getting inverter status.""" # Load IEEE13 feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Add a test PV system dss.Text.Command( "New PVSystem.TestPV4 Bus1=675 Phases=3 kV=4.16 kVA=500 Pmpp=500 irradiance=1.0" ) # Solve power flow dss.Solution.Solve() assert dss.Solution.Converged() # Get inverter status status = get_inverter_status("TestPV4") # Verify status structure assert status["success"], "Status retrieval should succeed" assert status["pv_name"] == "TestPV4" assert "kw" in status assert "kvar" in status assert "kva" in status assert "pf" in status assert "voltage_pu" in status # Verify power output is reasonable # Note: kW may be negative due to sign convention (negative = generating) assert abs(status["kw"]) > 0, "PV should be generating real power" assert status["kva"] >= abs(status["kw"]), "kVA should be >= |kW|" assert 0 <= abs(status["pf"]) <= 1.0, "Power factor should be between 0 and 1" assert status["voltage_pu"] > 0.9, "Voltage should be reasonable" def test_get_inverter_status_with_volt_var(): """Test getting inverter status with volt-var control active.""" # Load IEEE13 feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Add a test PV system dss.Text.Command( "New PVSystem.TestPV5 Bus1=675 Phases=3 kV=4.16 kVA=500 Pmpp=500 irradiance=1.0" ) # Configure volt-var control curve_points = load_curve("IEEE1547") configure_volt_var_control("TestPV5", curve_points, response_time=10.0) # Solve power flow dss.Solution.Solve() assert dss.Solution.Converged() # Get inverter status status = get_inverter_status("TestPV5") # Verify status assert status["success"] assert abs(status["kw"]) > 0, "PV should be generating real power" # Note: kvar may be non-zero due to volt-var control # The exact value depends on the voltage at the bus def test_configure_multiple_inverters(): """Test configuring multiple inverters with different curves.""" # Load IEEE13 feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Add multiple PV systems dss.Text.Command("New PVSystem.PV_A Bus1=675 Phases=3 kV=4.16 kVA=400 Pmpp=400") dss.Text.Command("New PVSystem.PV_B Bus1=611 Phases=3 kV=4.16 kVA=300 Pmpp=300") dss.Text.Command("New PVSystem.PV_C Bus1=652 Phases=1 kV=2.4 kVA=200 Pmpp=200") # Configure with different curves ieee_curve = load_curve("IEEE1547") rule21_curve = load_curve("RULE21") configure_volt_var_control("PV_A", ieee_curve, response_time=10.0) configure_volt_var_control("PV_B", rule21_curve, response_time=5.0) configure_volt_var_control("PV_C", ieee_curve, response_time=15.0) # Verify all XYCurves were created all_curves = dss.XYCurves.AllNames() assert len(all_curves) >= 3, "Should have at least 3 XYCurves" # Solve (may have control iteration warnings with multiple controls, but that's OK) try: dss.Solution.Solve() # If it converges, great. If not, that's acceptable for this test # since we're just verifying the controls were configured except Exception: pass # Control iteration limits can be exceeded with multiple VVC controls def test_curve_points_format(): """Test that curve points are in correct format.""" # Load curve curve = load_curve("IEEE1547") # Verify format for voltage, var in curve: assert isinstance(voltage, (int, float)), "Voltage should be numeric" assert isinstance(var, (int, float)), "Var should be numeric" assert 0.8 <= voltage <= 1.2, "Voltage should be in reasonable range" assert -0.5 <= var <= 0.5, "Var should be in typical range" def test_load_curve_insufficient_points(): """Test that curves with < 2 points raise ValueError.""" import tempfile import json from pathlib import Path with tempfile.TemporaryDirectory() as tmpdir: # Create invalid curve with 1 point curve_file = Path(tmpdir) / "invalid_curve.json" with open(curve_file, "w") as f: json.dump({"points": [[0.95, 0.0]]}, f) with pytest.raises(ValueError, match="must have at least 2 points"): load_curve(str(curve_file)) def test_load_curve_missing_points_field(): """Test that curves without points field raise ValueError.""" import tempfile import json from pathlib import Path with tempfile.TemporaryDirectory() as tmpdir: # Create curve without points field curve_file = Path(tmpdir) / "no_points.json" with open(curve_file, "w") as f: json.dump({"name": "test", "type": "volt-var"}, f) with pytest.raises(ValueError, match="missing 'points' field"): load_curve(str(curve_file)) def test_load_curve_invalid_json(): """Test that invalid JSON raises ValueError.""" import tempfile from pathlib import Path with tempfile.TemporaryDirectory() as tmpdir: # Create file with invalid JSON curve_file = Path(tmpdir) / "bad.json" with open(curve_file, "w") as f: f.write("{ invalid json }") with pytest.raises(ValueError, match="Invalid JSON"): load_curve(str(curve_file)) def test_configure_volt_var_insufficient_points(): """Test that volt-var control with < 2 points raises error.""" load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] dss.Text.Command("New PVSystem.TestPV Bus1=675 Phases=3 kV=4.16 kVA=500") with pytest.raises(RuntimeError): configure_volt_var_control("TestPV", [(0.95, 0.0)], response_time=10.0) def test_configure_volt_watt_insufficient_points(): """Test that volt-watt control with < 2 points raises error.""" load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] dss.Text.Command("New PVSystem.TestPV Bus1=675 Phases=3 kV=4.16 kVA=500") with pytest.raises(RuntimeError): configure_volt_watt_control("TestPV", [(1.0, 1.0)]) def test_get_inverter_status_nonexistent_pv(): """Test getting status for non-existent PV system.""" load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] status = get_inverter_status("NONEXISTENT_PV") assert status["success"] is False assert len(status["errors"]) > 0 assert "not found" in status["errors"][0].lower() def test_configure_volt_watt_out_of_range_warning(caplog): """Test that watt values outside [0, 1] log a warning and raise RuntimeError.""" import logging caplog.set_level(logging.WARNING) load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] dss.Text.Command("New PVSystem.TestPV Bus1=675 Phases=3 kV=4.16 kVA=500") # Curve with value > 1.0 should log warning and then raise error from OpenDSS bad_curve = [(0.0, 1.0), (1.10, 1.5)] with pytest.raises(RuntimeError): configure_volt_watt_control("TestPV", bad_curve) # Check that warning was logged before the error assert any("outside typical range" in record.message for record in caplog.records) if __name__ == "__main__": pytest.main([__file__, "-v"])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ahmedelshazly27/opendss-mcp-server1'

If you have feedback or need assistance with the MCP directory API, please join our Discord server