Skip to main content
Glama
capacity.py13.6 kB
""" Feeder capacity analysis module for OpenDSS. This module provides functions for determining the maximum DER hosting capacity at a specific bus before constraint violations occur. """ import logging from typing import Any, Dict, List, Optional import opendssdirect as dss from ..utils.formatters import format_success_response, format_error_response from ..utils.validators import validate_positive_float from .voltage_checker import check_voltage_violations logger = logging.getLogger(__name__) # Maximum number of iterations to prevent infinite loops MAX_ITERATIONS = 1000 # Supported DER types SUPPORTED_DER_TYPES = ["solar", "battery", "wind"] def _check_line_loading() -> Dict[str, Any]: """Check if any lines exceed 100% loading. Returns: Dictionary with overloaded lines information: - has_overloads: bool - overloaded_lines: list of {line: str, loading_pct: float} - max_loading_pct: float """ overloaded_lines = [] max_loading = 0.0 try: # Iterate through all lines line_count = dss.Lines.Count() if line_count == 0: return { "has_overloads": False, "overloaded_lines": [], "max_loading_pct": 0.0, } dss.Lines.First() for _ in range(line_count): line_name = dss.Lines.Name() # Get line currents and normal ampacity dss.Circuit.SetActiveElement(f"Line.{line_name}") currents = dss.CktElement.CurrentsMagAng() normal_amps = dss.CktElement.NormalAmps() if normal_amps > 0 and currents: # Get maximum phase current (currents are interleaved mag/angle) phase_currents = [currents[i] for i in range(0, len(currents), 2)] max_current = max(phase_currents) if phase_currents else 0.0 # Calculate loading percentage loading_pct = (max_current / normal_amps) * 100.0 if loading_pct > max_loading: max_loading = loading_pct if loading_pct > 100.0: overloaded_lines.append( {"line": line_name, "loading_pct": round(loading_pct, 2)} ) dss.Lines.Next() except Exception as e: logger.error(f"Error checking line loading: {e}") return { "has_overloads": len(overloaded_lines) > 0, "overloaded_lines": overloaded_lines, "max_loading_pct": round(max_loading, 2), } def _add_der(bus_id: str, der_type: str, capacity_kw: float) -> bool: """Add a DER to the specified bus. Args: bus_id: Bus identifier der_type: Type of DER ("solar", "battery", "wind") capacity_kw: DER capacity in kW Returns: bool: True if DER was added successfully """ try: # Get bus voltage base dss.Circuit.SetActiveBus(bus_id) kv_base = dss.Bus.kVBase() if kv_base == 0: logger.error(f"Bus {bus_id} has zero voltage base") return False # Create unique DER name der_name = f"der_test_{bus_id}" if der_type == "solar": # Add PV system dss.Text.Command( f"New PVSystem.{der_name} Bus1={bus_id} kV={kv_base} kVA={capacity_kw} Pmpp={capacity_kw} irradiance=1.0" ) elif der_type == "battery": # Add storage dss.Text.Command( f"New Storage.{der_name} Bus1={bus_id} kV={kv_base} kWrated={capacity_kw} kWhrated={capacity_kw * 4} %stored=100 %discharge=100" ) elif der_type == "wind": # Add generator (simplified wind model) dss.Text.Command( f"New Generator.{der_name} Bus1={bus_id} kV={kv_base} kW={capacity_kw} PF=1.0" ) else: logger.error(f"Unsupported DER type: {der_type}") return False return True except Exception as e: logger.error(f"Error adding DER: {e}") return False def _remove_der(bus_id: str) -> None: """Remove test DER from the circuit. Args: bus_id: Bus identifier """ try: der_name = f"der_test_{bus_id}" # Try to disable all possible DER types for element_type in ["PVSystem", "Storage", "Generator"]: try: dss.Text.Command(f"{element_type}.{der_name}.enabled=no") except: pass # Element might not exist except Exception as e: logger.error(f"Error removing DER: {e}") def analyze_feeder_capacity( bus_id: str, der_type: str = "solar", increment_kw: float = 100, max_capacity_kw: float = 10000, constraints: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Analyze maximum DER hosting capacity at a specific bus. This function performs an iterative capacity analysis by incrementally adding DER capacity at the specified bus and checking for constraint violations (voltage limits and line loading). The analysis stops when a violation is detected or the maximum capacity is reached. Args: bus_id: Identifier of the bus where DER will be connected der_type: Type of DER to analyze - "solar", "battery", or "wind" (default: "solar") increment_kw: Capacity increment for each iteration in kW (default: 100) max_capacity_kw: Maximum capacity to test in kW (default: 10000) constraints: Optional dictionary of constraint limits: - min_voltage_pu: Minimum voltage limit (default: 0.95) - max_voltage_pu: Maximum voltage limit (default: 1.05) - max_line_loading_pct: Maximum line loading (default: 100%) Returns: Dictionary containing: - success: Boolean indicating if the operation was successful - data: Dictionary with capacity analysis results - metadata: Additional metadata about the analysis - errors: List of error messages if any occurred Example: >>> result = analyze_feeder_capacity("675", der_type="solar", increment_kw=100) >>> if result['success']: ... max_capacity = result['data']['max_capacity_kw'] ... print(f"Maximum capacity: {max_capacity} kW") """ try: # Validate inputs validate_positive_float(increment_kw, "increment_kw") validate_positive_float(max_capacity_kw, "max_capacity_kw") if der_type not in SUPPORTED_DER_TYPES: return format_error_response( f"Unsupported DER type '{der_type}'. Supported types: {', '.join(SUPPORTED_DER_TYPES)}" ) if increment_kw > max_capacity_kw: return format_error_response( f"increment_kw ({increment_kw}) cannot be greater than max_capacity_kw ({max_capacity_kw})" ) # Check if circuit is loaded if not dss.Circuit.Name(): return format_error_response( "No circuit loaded. Please load a feeder first using load_feeder tool." ) # Validate bus exists all_buses = [bus.lower() for bus in dss.Circuit.AllBusNames()] if bus_id.lower() not in all_buses: return format_error_response( f"Bus '{bus_id}' not found in circuit. Available buses: {', '.join(all_buses[:5])}..." ) # Parse constraints constraints = constraints or {} min_voltage_pu = constraints.get("min_voltage_pu", 0.95) max_voltage_pu = constraints.get("max_voltage_pu", 1.05) max_line_loading_pct = constraints.get("max_line_loading_pct", 100.0) # Get baseline (no DER) metrics dss.Solution.Solve() if not dss.Solution.Converged(): return format_error_response("Baseline power flow did not converge") baseline_voltage_check = check_voltage_violations( min_voltage_pu, max_voltage_pu ) baseline_loading = _check_line_loading() # Initialize capacity curve data capacity_curve: List[Dict[str, Any]] = [] max_capacity_reached = 0.0 limiting_constraint = None violation_details = None # Iterative capacity analysis capacity = 0.0 iteration = 0 while capacity <= max_capacity_kw and iteration < MAX_ITERATIONS: iteration += 1 # Add DER at current capacity if not _add_der(bus_id, der_type, capacity): return format_error_response(f"Failed to add DER at bus {bus_id}") # Run power flow dss.Solution.Solve() if not dss.Solution.Converged(): # Power flow didn't converge - capacity limit reached _remove_der(bus_id) limiting_constraint = "convergence_failure" violation_details = "Power flow solution did not converge" break # Check voltage violations voltage_check = check_voltage_violations(min_voltage_pu, max_voltage_pu) has_voltage_violations = ( voltage_check.get("success", False) and voltage_check.get("data", {}) .get("summary", {}) .get("total_violations", 0) > 0 ) # Check line loading loading_check = _check_line_loading() has_loading_violations = ( loading_check["has_overloads"] or loading_check["max_loading_pct"] > max_line_loading_pct ) # Store iteration data iteration_data = { "capacity_kw": round(capacity, 2), "converged": True, "voltage_violations": voltage_check.get("data", {}) .get("summary", {}) .get("total_violations", 0), "max_line_loading_pct": loading_check["max_loading_pct"], "has_violations": has_voltage_violations or has_loading_violations, } capacity_curve.append(iteration_data) # Check for violations if has_voltage_violations or has_loading_violations: # Violation detected - capacity limit reached _remove_der(bus_id) if has_voltage_violations: limiting_constraint = "voltage_violation" worst = ( voltage_check.get("data", {}) .get("summary", {}) .get("worst_violation") ) if worst: violation_details = f"Voltage violation at bus {worst['bus']} phase {worst['phase']}: {worst['voltage_pu']} pu" else: violation_details = "Voltage limit exceeded" else: limiting_constraint = "line_overload" overloaded = loading_check["overloaded_lines"] if overloaded: line_info = overloaded[0] violation_details = f"Line {line_info['line']} overloaded: {line_info['loading_pct']}%" else: violation_details = ( f"Line loading exceeded {max_line_loading_pct}%" ) break # No violations - update max capacity and continue max_capacity_reached = capacity _remove_der(bus_id) capacity += increment_kw # Remove test DER _remove_der(bus_id) # Prepare results data = { "bus_id": bus_id, "der_type": der_type, "max_capacity_kw": round(max_capacity_reached, 2), "limiting_constraint": limiting_constraint, "violation_details": violation_details, "capacity_curve": capacity_curve, "baseline": { "voltage_violations": baseline_voltage_check.get("data", {}) .get("summary", {}) .get("total_violations", 0), "max_line_loading_pct": baseline_loading["max_loading_pct"], }, "constraints": { "min_voltage_pu": min_voltage_pu, "max_voltage_pu": max_voltage_pu, "max_line_loading_pct": max_line_loading_pct, }, "analysis_parameters": { "increment_kw": increment_kw, "max_capacity_tested_kw": max_capacity_kw, "iterations_performed": iteration, }, } metadata = { "circuit_name": dss.Circuit.Name(), "analysis_type": "hosting_capacity", } return format_success_response(data, metadata) except ValueError as e: return format_error_response(str(e)) except Exception as e: error_msg = f"Error analyzing feeder capacity: {str(e)}" logger.exception(error_msg) return format_error_response(error_msg)

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