import numpy as np import random import math class CoreCircuit: """ Encapsulates the core logic for circuit generation and solving. """ @staticmethod def generate_random_graph_edges(n_nodes: int, seed: int | None = None) -> list: """ Generates a list of edges for a random graph with n_nodes. Each edge is (R, E, u, v). Ensures the graph is connected if n_nodes > 1. Seedable for reproducibility. """ print(f"[DEBUG libcircuit] generate_random_graph_edges called with n_nodes={n_nodes}, seed={seed}") if seed is not None: np.random.seed(seed) print(f"[DEBUG libcircuit] NumPy random seed set to {seed}") if n_nodes == 0: print("[DEBUG libcircuit] n_nodes is 0, returning []") return [] if n_nodes == 1: print("[DEBUG libcircuit] n_nodes is 1, returning []") return [] # No edges for a single node typically in circuit problems unless specified edges = [] min_r_int, max_r_int = 1, 10 # Changed to int min_e_int, max_e_int = -10, 10 # Changed to int # Ensure connectivity for n_nodes > 1 using a spanning tree (line graph) nodes = list(range(n_nodes)) np.random.shuffle(nodes) # Shuffle to make the spanning tree random for i in range(n_nodes - 1): u, v = nodes[i], nodes[i+1] R = np.random.randint(min_r_int, max_r_int + 1) # Generate integer R E = np.random.randint(min_e_int, max_e_int + 1) # Generate integer E edges.append((R, E, u, v)) print(f"[DEBUG libcircuit] Edges after spanning tree: {len(edges)}") max_possible_edges = n_nodes * (n_nodes -1) // 2 print(f"[DEBUG libcircuit] max_possible_edges: {max_possible_edges}") if n_nodes > 2: num_additional_edges_max = max(0, min(int(n_nodes * 0.5), max_possible_edges - (n_nodes - 1))) print(f"[DEBUG libcircuit] num_additional_edges_max: {num_additional_edges_max}") num_target_additional_edges = 0 if num_additional_edges_max > 0: num_target_additional_edges = np.random.randint(2, num_additional_edges_max + 1) print(f"[DEBUG libcircuit] num_target_additional_edges: {num_target_additional_edges}") existing_pairs = set() for _, _, u, v in edges: existing_pairs.add(tuple(sorted((u,v)))) successfully_added_edges = 0 max_attempts_per_edge = n_nodes * n_nodes # A relatively loose attempt limit if num_target_additional_edges > 0: print(f"[DEBUG libcircuit] Attempting to add {num_target_additional_edges} additional edges.") for i in range(num_target_additional_edges): if len(existing_pairs) >= max_possible_edges: print("[DEBUG libcircuit] Graph is full, cannot add more edges.") break # Graph is full current_attempts = 0 added_this_iteration = False while current_attempts < max_attempts_per_edge: u, v = np.random.choice(range(n_nodes), 2, replace=False) if tuple(sorted((u, v))) not in existing_pairs: R = np.random.randint(min_r_int, max_r_int + 1) # Generate integer R E = np.random.randint(min_e_int, max_e_int + 1) # Generate integer E edges.append((R, E, u, v)) existing_pairs.add(tuple(sorted((u, v)))) successfully_added_edges += 1 added_this_iteration = True print(f"[DEBUG libcircuit] Successfully added additional edge #{successfully_added_edges} (target {i+1}/{num_target_additional_edges}). Total edges: {len(edges)}") break # Successfully added, break from while current_attempts += 1 if not added_this_iteration: print(f"[DEBUG libcircuit] Failed to add additional edge target {i+1}/{num_target_additional_edges} after {max_attempts_per_edge} attempts.") # Optional: log a warning if an edge couldn't be added despite many attempts # print(f"Warning: Could not add target additional edge after {max_attempts_per_edge} attempts.") # break # Stop trying to add more additional edges if one attempt fails badly else: print("[DEBUG libcircuit] num_target_additional_edges is 0, no additional edges will be attempted.") print(f"[DEBUG libcircuit] Total successfully_added_edges: {successfully_added_edges}") else: print("[DEBUG libcircuit] n_nodes is not > 2, skipping additional edge logic.") print(f"[DEBUG libcircuit] Returning {len(edges)} edges.") return edges @staticmethod def solve_circuit_potentials_and_currents(n_nodes: int, edges: list, seed: int | None = None): """ Solves a circuit using MNA to find all node potentials (node 0 as reference) and all branch currents. Args: n_nodes (int): Number of nodes in the circuit. edges (list): List of edges, where each edge is a tuple (R, E, u, v). R: resistance, E: EMF (+ terminal at v), u: start node, v: end node. seed (int, optional): Not used in this deterministic calculation, but kept for API consistency. Returns: tuple: (branch_currents, node_potentials) branch_currents (list[float] | None): List of currents for each edge in the input 'edges' list. Positive current flows from u to v as defined by the edge tuple. node_potentials (list[float] | None): List of potentials for each node (0 to n_nodes-1). node_potentials[0] is always 0.0. Returns (None, None) if the circuit is unsolvable or invalid. """ if seed is not None: # np.random.seed(seed) # Not strictly needed for np.linalg.solve pass # Seed is not used in this deterministic MNA solver if n_nodes < 1: return None, None # Validate edges: R > 0 and valid node indices for R_val, _, u_node, v_node in edges: if R_val <= 0: # Resistances must be positive for this MNA formulation return None, None if not (0 <= u_node < n_nodes and 0 <= v_node < n_nodes): return None, None # Invalid node indices if n_nodes == 1: # Only node 0 exists, its potential is 0. node_potentials = [0.0] branch_currents = [] for R_k, E_k, u_k, v_k in edges: # For n_nodes=1, valid edges must be (0,0) due to above validation if u_k == 0 and v_k == 0: # Edge from node 0 to node 0 # Current I = (V0 - V0 + E_k) / R_k = E_k / R_k branch_currents.append(E_k / R_k) # else: This path should not be reachable if validation is correct return branch_currents, node_potentials ref_node = 0 num_voltage_vars = n_nodes - 1 # We solve for V_1, ..., V_{n_nodes-1} # If there are no unknown potentials (e.g. n_nodes=1 already handled, but for safety if num_voltage_vars becomes 0) if num_voltage_vars == 0: # Should be covered by n_nodes == 1 case all_node_potentials_trivial = [0.0] * n_nodes # all_node_potentials[0] = 0 branch_currents_trivial = [] for R_k, E_k, u_k, v_k in edges: V_u_k = all_node_potentials_trivial[u_k] V_v_k = all_node_potentials_trivial[v_k] current_k = (V_u_k - V_v_k + E_k) / R_k branch_currents_trivial.append(current_k) return branch_currents_trivial, all_node_potentials_trivial M = np.zeros((num_voltage_vars, num_voltage_vars)) Z = np.zeros(num_voltage_vars) # Populate MNA matrices based on KCL # For an edge (u,v) with R_uv, E_uv (+ at v): # Current I_uv = (V_u - V_v + E_uv) / R_uv # This can be seen as (V_u - V_v)/R_uv + E_uv/R_uv # The term E_uv/R_uv is an equivalent current source from u to v. for R, E_uv, u, v in edges: g = 1.0 / R J_eq = E_uv * g # Equivalent current source from u to v due to E_uv # Matrix indices map from node numbers (1 to n_nodes-1) to (0 to n_nodes-2) if u != ref_node: u_idx = u - 1 # Node u (if not ref) corresponds to (u-1)-th unknown potential V_u M[u_idx, u_idx] += g Z[u_idx] -= J_eq # J_eq flows out of node u, hence negative on RHS of KCL sum_Ig = sum_Is if v != ref_node: v_idx = v - 1 # Node v (if not ref) corresponds to (v-1)-th unknown potential V_v M[v_idx, v_idx] += g Z[v_idx] += J_eq # J_eq flows into node v, hence positive on RHS if u != ref_node and v != ref_node: u_idx = u - 1 v_idx = v - 1 M[u_idx, v_idx] -= g M[v_idx, u_idx] -= g # Solve for unknown node potentials (V_1 to V_{n_nodes-1}) solved_potentials_unknowns = np.zeros(num_voltage_vars) try: solved_potentials_unknowns = np.linalg.solve(M, Z) if np.any(np.isnan(solved_potentials_unknowns)) or np.any(np.isinf(solved_potentials_unknowns)): return None, None # Unstable solution except np.linalg.LinAlgError: # e.g. singular matrix if circuit is ill-defined return None, None # Construct full list of node potentials (V_0 to V_{n_nodes-1}) all_node_potentials = [0.0] * n_nodes # V_0 (all_node_potentials[0]) is 0.0 by definition for i in range(num_voltage_vars): all_node_potentials[i + 1] = solved_potentials_unknowns[i] # V_1, V_2 ... # Calculate branch currents using solved potentials branch_currents = [] for R_k, E_k, u_k, v_k in edges: # E_k is EMF, positive if + terminal at v_k V_u_k = all_node_potentials[u_k] V_v_k = all_node_potentials[v_k] # Current I_k from u_k to v_k is (V_u_k - V_v_k + E_k) / R_k current_k = (V_u_k - V_v_k + E_k) / R_k branch_currents.append(current_k) return branch_currents, all_node_potentials