Skip to main content
Glama
test_integration.py12.2 kB
""" Integration tests for the OpenDSS MCP Server. This module contains end-to-end tests that exercise the complete workflow from the PRD use case: DER integration study with volt-var control. """ import pytest from opendss_mcp.tools.feeder_loader import load_ieee_test_feeder from opendss_mcp.tools.power_flow import run_power_flow from opendss_mcp.tools.voltage_checker import check_voltage_violations from opendss_mcp.tools.der_optimizer import optimize_der_placement from opendss_mcp.tools.capacity import analyze_feeder_capacity from opendss_mcp.tools.timeseries import run_time_series_simulation from opendss_mcp.tools.visualization import generate_visualization @pytest.fixture(scope="module") def ieee13_loaded(): """Fixture to load IEEE13 feeder once for all integration tests.""" result = load_ieee_test_feeder("IEEE13") assert result["success"], f"Failed to load IEEE13 feeder: {result.get('errors')}" return result def test_full_der_study(ieee13_loaded): """ Test complete DER integration study workflow from PRD use case. This integration test exercises all 7 tools in a realistic workflow: 1. Load IEEE13 feeder (via fixture) 2. Run baseline power flow analysis 3. Check for voltage violations 4. Optimize DER placement with volt-var control 5. Verify system improvement metrics 6. Analyze hosting capacity at optimal location 7. Run time-series simulation with DER 8. Generate visualization of results This simulates a real distribution planning study reduced from weeks to minutes. """ # Step 1: Feeder already loaded via fixture feeder_data = ieee13_loaded assert feeder_data["data"]["feeder_id"] == "IEEE13" print( f"\n✓ Step 1: IEEE13 feeder loaded ({feeder_data['data']['num_buses']} buses)" ) # Step 2: Run baseline power flow print("\n✓ Step 2: Running baseline power flow...") pf_result = run_power_flow("IEEE13") assert pf_result["success"], f"Power flow failed: {pf_result.get('errors')}" assert pf_result["data"]["converged"], "Power flow did not converge" # Get baseline metrics for comparison baseline_min_voltage = pf_result["data"]["min_voltage"] baseline_max_voltage = pf_result["data"]["max_voltage"] print( f" - Voltage range: {baseline_min_voltage:.4f} - {baseline_max_voltage:.4f} pu" ) print(f" - Converged in {pf_result['data']['iterations']} iterations") # Step 3: Check voltage violations print("\n✓ Step 3: Checking voltage violations...") vio_result = check_voltage_violations(min_voltage_pu=0.95, max_voltage_pu=1.05) assert vio_result["success"], f"Voltage check failed: {vio_result.get('errors')}" baseline_violations = vio_result["data"]["summary"]["total_violations"] print(f" - Found {baseline_violations} voltage violations") # Step 4: Optimize DER placement # Note: Using "solar" instead of "solar_vvc" as the actual tool implementation may vary print("\n✓ Step 4: Optimizing DER placement (2000 kW solar)...") der_result = optimize_der_placement( der_type="solar", capacity_kw=2000, objective="minimize_losses", constraints={"max_candidates": 5}, # Limit for faster test ) assert der_result["success"], f"DER optimization failed: {der_result.get('errors')}" optimal_bus = der_result["data"]["optimal_bus"] improvement = der_result["data"]["improvement_metrics"] print(f" - Optimal bus: {optimal_bus}") print(f" - Loss reduction: {improvement.get('loss_reduction_kw', 0):.2f} kW") print(f" - Loss reduction: {improvement.get('loss_reduction_pct', 0):.2f}%") # Step 5: Verify improvement # The optimization should provide some benefit (or at least not make things worse) assert "loss_reduction_pct" in improvement, "Missing loss_reduction_pct in results" # Note: Some configurations may not show improvement, so we check >= 0 assert improvement["loss_reduction_pct"] >= 0, "DER placement made losses worse" print(f"\n✓ Step 5: System improvement verified") # Step 6: Analyze capacity at optimal location print(f"\n✓ Step 6: Analyzing hosting capacity at bus {optimal_bus}...") cap_result = analyze_feeder_capacity( bus_id=optimal_bus, der_type="solar", increment_kw=500, # Larger increment for faster test max_capacity_kw=5000, # Lower max for faster test constraints={"max_voltage_pu": 1.05}, ) assert cap_result[ "success" ], f"Capacity analysis failed: {cap_result.get('errors')}" max_capacity = cap_result["data"]["max_capacity_kw"] limiting_constraint = cap_result["data"]["limiting_constraint"] print(f" - Maximum capacity: {max_capacity} kW") print(f" - Limited by: {limiting_constraint}") # Capacity should be at least as much as we placed (2000 kW) or zero if no capacity # Zero capacity is possible if the bus cannot host any DER assert max_capacity >= 0, "Capacity should be non-negative" # Step 7: Run time-series simulation print("\n✓ Step 7: Running time-series simulation (simulated profiles)...") # Create simple load and generation profiles for testing # 24-hour profiles with typical daily patterns import numpy as np # Generate hourly load profile (higher during day, lower at night) hours = list(range(24)) load_profile = { "name": "test_residential", "multipliers": [0.6 + 0.4 * np.sin((h - 6) * np.pi / 12) for h in hours], } # Generate solar profile (peak at noon, zero at night) gen_profile = { "name": "test_solar", "multipliers": [max(0, np.sin((h - 6) * np.pi / 12)) for h in hours], } ts_result = run_time_series_simulation( load_profile=load_profile, generation_profile=gen_profile, duration_hours=24, timestep_minutes=60, output_variables=["voltages", "losses"], ) assert ts_result[ "success" ], f"Time-series simulation failed: {ts_result.get('errors')}" summary = ts_result["data"]["summary"] print(f" - Timesteps: {summary['num_timesteps']}") print(f" - Average losses: {summary.get('avg_losses_kw', 0):.2f} kW") print(f" - Peak load: {summary.get('peak_load_kw', 0):.2f} kW") print( f" - Voltage range: {summary.get('min_voltage_pu', 0):.4f} - {summary.get('max_voltage_pu', 0):.4f} pu" ) print(f" - Convergence rate: {summary.get('convergence_rate_pct', 0):.1f}%") # Verify time-series ran for full duration assert summary["num_timesteps"] == 24, "Should have 24 hourly timesteps" assert summary["convergence_rate_pct"] > 50, "Most timesteps should converge" # Step 8: Generate visualization print("\n✓ Step 8: Generating voltage profile visualization...") viz_result = generate_visualization( plot_type="voltage_profile", data_source="circuit", options={ "title": "Integration Test - IEEE123 Voltage Profile", "figsize": (10, 6), }, ) assert viz_result["success"], f"Visualization failed: {viz_result.get('errors')}" # Verify visualization was created assert "plot_type" in viz_result["data"] assert viz_result["data"]["plot_type"] == "voltage_profile" # Check that we got either a file_path or base64 image has_output = ( viz_result["data"].get("file_path") is not None or viz_result["data"].get("image_base64") is not None ) assert has_output, "Visualization should produce either file or base64 output" print(f" - Visualization created successfully") # Final summary print("\n" + "=" * 70) print("INTEGRATION TEST COMPLETE - Full DER Study Workflow") print("=" * 70) print(f"✓ Loaded {feeder_data['data']['num_buses']}-bus IEEE123 test feeder") print( f"✓ Baseline power flow: {baseline_min_voltage:.4f}-{baseline_max_voltage:.4f} pu" ) print(f"✓ Voltage violations: {baseline_violations}") print(f"✓ Optimal DER location: Bus {optimal_bus}") print(f"✓ Loss reduction: {improvement.get('loss_reduction_pct', 0):.2f}%") print(f"✓ Hosting capacity: {max_capacity} kW (limited by {limiting_constraint})") print( f"✓ Time-series: {summary['num_timesteps']} timesteps, {summary.get('convergence_rate_pct', 0):.1f}% convergence" ) print(f"✓ Visualization: voltage_profile generated") print("=" * 70) print("\nAll workflow steps completed successfully!") print("Distribution planning study reduced from weeks to minutes! 🎉") def test_basic_workflow(): """Test basic 3-step workflow: load, power flow, voltage check.""" # Step 1: Load feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] print(f"\n✓ Loaded IEEE13 feeder") # Step 2: Run power flow pf_result = run_power_flow("IEEE13") assert pf_result["success"] assert pf_result["data"]["converged"] print(f"✓ Power flow converged") # Step 3: Check voltages vio_result = check_voltage_violations() assert vio_result["success"] print( f"✓ Voltage check completed ({vio_result['data']['summary']['total_violations']} violations)" ) def test_visualization_workflow(): """Test visualization generation from circuit state.""" # Load feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Run power flow pf_result = run_power_flow("IEEE13") assert pf_result["success"] # Generate voltage profile viz_result = generate_visualization( plot_type="voltage_profile", data_source="circuit" ) assert viz_result["success"] assert viz_result["data"]["plot_type"] == "voltage_profile" print("\n✓ Voltage profile visualization generated") # Generate network diagram viz_result2 = generate_visualization( plot_type="network_diagram", data_source="circuit" ) assert viz_result2["success"] assert viz_result2["data"]["plot_type"] == "network_diagram" print("✓ Network diagram visualization generated") def test_der_optimization_workflow(): """Test DER optimization with different objectives.""" # Load feeder load_result = load_ieee_test_feeder("IEEE13") assert load_result["success"] # Optimize for loss minimization der_result = optimize_der_placement( der_type="solar", capacity_kw=1000, objective="minimize_losses", constraints={"max_candidates": 3}, ) assert der_result["success"] optimal_bus = der_result["data"]["optimal_bus"] print(f"\n✓ DER optimization (minimize_losses): bus {optimal_bus}") # Verify we got improvement metrics assert "improvement_metrics" in der_result["data"] assert "baseline" in der_result["data"] assert "comparison_table" in der_result["data"] def test_response_format_consistency(): """Test that all tools return consistent response format.""" # Load feeder load_result = load_ieee_test_feeder("IEEE13") # All tools should return these keys required_keys = {"success", "data", "metadata", "errors"} # Test each tool's response format tools_to_test = [ ("load_feeder", load_result), ("power_flow", run_power_flow("IEEE13")), ("voltage_check", check_voltage_violations()), ] for tool_name, result in tools_to_test: assert ( set(result.keys()) == required_keys ), f"{tool_name} response missing required keys" assert isinstance(result["success"], bool) if result["success"]: assert result["data"] is not None else: assert isinstance(result["errors"], list) print("\n✓ All tools return consistent response format")

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