diff --git a/examples/pipelines/puzzle_configs/circuit_test.json b/examples/pipelines/puzzle_configs/circuit_test.json new file mode 100644 index 0000000..60c35f8 --- /dev/null +++ b/examples/pipelines/puzzle_configs/circuit_test.json @@ -0,0 +1,7 @@ +[ + { + "min_nodes": 3, + "max_nodes": 6 + } + +] \ No newline at end of file diff --git a/examples/pipelines/puzzle_configs/circuit_train.json b/examples/pipelines/puzzle_configs/circuit_train.json new file mode 100644 index 0000000..60c35f8 --- /dev/null +++ b/examples/pipelines/puzzle_configs/circuit_train.json @@ -0,0 +1,7 @@ +[ + { + "min_nodes": 3, + "max_nodes": 6 + } + +] \ No newline at end of file diff --git a/internbootcamp/bootcamp/__init__.py b/internbootcamp/bootcamp/__init__.py index 2041d37..58067ac 100755 --- a/internbootcamp/bootcamp/__init__.py +++ b/internbootcamp/bootcamp/__init__.py @@ -11,6 +11,7 @@ from .cipher.cipher_default import Cipherbootcamp from .crypto_math.crypto_math import Cryptomathbootcamp from .futoshiki.futoshiki import Futoshikibootcamp from .game24.game_default import Game24bootcamp +from .circuit.circuit import Circuitbootcamp from .skyscrapers.skyscrapers import Skyscrapersbootcamp from .starbattle.starbattle_default import Starbattlebootcamp # from .masyu.masyu_default import Masyubootcamp diff --git a/internbootcamp/bootcamp/circuit/circuit.py b/internbootcamp/bootcamp/circuit/circuit.py new file mode 100644 index 0000000..a2cbbae --- /dev/null +++ b/internbootcamp/bootcamp/circuit/circuit.py @@ -0,0 +1,1524 @@ +import numpy as np +import random +import math +import re +import os +from typing import Optional, Dict, List, Tuple, Any +# Attempt to import Basebootcamp, assuming the path is accessible +try: + from internbootcamp.bootcamp.base import Basebootcamp +except ImportError: + # Fallback if the specific path is not found, e.g. for local testing + # This might need adjustment based on the actual execution environment + class Basebootcamp: + def __init__(self, *args, **kwargs): + pass + def case_generator(self): + raise NotImplementedError + def prompt_func(self, identity): + raise NotImplementedError + @staticmethod + def extract_output(output_str): + raise NotImplementedError + @classmethod + def _verify_correction(cls, solution, identity): + raise NotImplementedError + +# Import the new core logic class +# Assuming the path is relative to the workspace root or correctly configured in PYTHONPATH +from internbootcamp.libs.circuit.libcircuit import CoreCircuit + +class Circuitbootcamp(Basebootcamp): + def __init__(self, min_nodes=3, max_nodes=6, seed=None): + """ + Initializes the circuit bootcamp. + Args: + min_nodes (int): Minimum number of nodes for generated circuits. + max_nodes (int): Maximum number of nodes for generated circuits. + seed (int, optional): Seed for random number generation. + """ + super().__init__() + self.min_nodes = max(2, min_nodes) # Ensure at least 2 nodes + self.max_nodes = max(self.min_nodes, max_nodes) + self.seed = seed # Store the seed + if self.seed is not None: + random.seed(self.seed) # Seed for random operations within Circuitbootcamp itself + # np.random.seed(self.seed) # Not strictly needed here as CoreCircuit handles its numpy seeding + + def case_generator(self) -> dict: + """ + 生成一个电路问题:n_nodes, edges, branch_currents 和 node_potentials。 + branch_currents:原始电路每条边上的电流列表 + node_potentials:每个节点的电势列表(节点0的电势为参考0V) + """ + # n_nodes is already a Python int due to random.randint + n_nodes = random.randint(self.min_nodes, self.max_nodes) + + # 使用 CoreCircuit 生成图,传递种子 + # edges from CoreCircuit will have u,v as numpy integers + original_edges = CoreCircuit.generate_random_graph_edges(n_nodes, seed=self.seed) + print(f"[DEBUG circuit] Generated original_edges: {original_edges}") + + # 转换 edges 中的 u, v 为 Python int 类型 + processed_edges = [] + if original_edges: + for edge in original_edges: + R, E, u, v = edge + processed_edges.append((R, E, int(u), int(v))) + + # 使用 CoreCircuit 求解电路,获取每条支路的电流和每个节点的电势 + # 注意:solve_circuit_potentials_and_currents 需要原始的 edges (如果它内部依赖特定类型,尽管通常数值计算库可以处理) + # 但为了安全和一致性,传递处理过的或者确保 solve_circuit_potentials_and_currents 也能处理 Python int 节点 + # 从 libcircuit.py 的实现看,它在MNA矩阵构建时使用 u,v 作为索引,Python int 也可以。 + branch_currents, node_potentials = CoreCircuit.solve_circuit_potentials_and_currents(n_nodes, processed_edges, seed=self.seed) + + # 确保 branch_currents 和 node_potentials 中的浮点数是标准 Python float (numpy floats 也能序列化,但为了彻底) + safe_branch_currents = None + if branch_currents is not None: + safe_branch_currents = [float(bc) if bc is not None else None for bc in branch_currents] + + safe_node_potentials = None + if node_potentials is not None: + safe_node_potentials = [float(np) if np is not None else None for np in node_potentials] + + return { + 'n_nodes': int(n_nodes), # 确保是 Python int + 'edges': processed_edges, + 'branch_currents': safe_branch_currents, + 'node_potentials': safe_node_potentials + } + + def prompt_func(self, identity: dict) -> str: + """ + 根据电路问题生成提示语,要求计算每条边上的电流和每个节点的电势。 + """ + n_nodes = identity['n_nodes'] + edges_str_list = [] + for i, edge_data in enumerate(identity['edges']): + R, E, u, v = edge_data + edges_str_list.append(f" Edge {i+1}: R={R:.2f} Ohm, E={E:.2f} V, in branch {u}-{v} (E is the Electromotive Force in branch {u}-{v}; positive if the source's positive terminal is at node {v} and negative terminal at node {u}.)") + edges_presentation = "\n".join(edges_str_list) if edges_str_list else " No existing edges." + + instruction = ( + f"Consider an electrical circuit with {n_nodes} nodes, labeled 0 to {n_nodes-1}. " + f"The circuit has the following edges:\\n{edges_presentation}\\n" + f"For this circuit, your task is to formulate the set of equations based on Kirchhoff's Laws that can be used to solve for all branch currents.\\n" + f"You are NOT required to solve these equations or provide the numerical values for currents.\\n" + ) + + instruction_following = ( + "Let's think step by step. Follow these instructions to formulate the equations:\n\n" + "1. **Analyze the Circuit Structure:** Identify all nodes and branches in the circuit. Determine how many independent loops exist.\n" + "2. **Formulate Equations using Kirchhoff's Laws with Branch Currents as Unknowns:**\n" + " - Assign a branch current variable to each edge. I_1 represents the current through Edge 1, I_2 represents the current through Edge 2, and so on. The assumed direction of each current aligns with the u -> v direction of the edge definition as provided: 'positive current is defined to flow from the first node towards the second node listed in the edge description'.\n" + " - Apply Kirchhoff's Current Law (KCL) at n-1 independent nodes (where n is the total number of nodes) to get a set of equations.\n" + " - Apply Kirchhoff's Voltage Law (KVL) around each independent loop to get another set of equations. Ensure you correctly account for the voltage drops across resistors (V=IR) and the EMFs of voltage sources (E), paying attention to their polarities relative to the loop traversal direction.\n" + "3. **Output the Equations:** Use the following format for your answer, listing all formulated KCL and KVL equations clearly:\n\n" + "```\n" + "Equations:\n" + "KCL at Node 1: \n" + "KCL at Node 2: \n" + "...\n" + "KVL for Loop 1: \n" + "KVL for Loop 2: \n" + "...\n" + "```\n\n" + "Focus solely on providing the correct set of equations based on the circuit description." + ) + prompt = instruction + '\n' + instruction_following + return prompt + + @staticmethod + def _parse_and_eval_equation(eq_str: str, true_branch_currents: List[Optional[float]], atol: float = 1e-2, rtol: float = 1e-3) -> bool: + # print(f"[DEBUG _parse_and_eval_equation] Evaluating equation: '{eq_str}' with currents: {true_branch_currents}") + if "=" not in eq_str: + print("[DEBUG _parse_and_eval_equation] No '=' found in equation string.") + return False + + lhs_str, rhs_str = eq_str.split('=', 1) + # print(f"[DEBUG _parse_and_eval_equation] LHS string: '{lhs_str}', RHS string: '{rhs_str}'") + + # Create a very limited scope for eval + # Only allow math constants and functions that don't interact with system + safe_globals = {"__builtins__": {}} + # Whitelist specific math functions if necessary, e.g. abs, sqrt, etc. + # For basic KCL/KVL, direct arithmetic should be fine. + # safe_locals = {name: getattr(math, name) for name in dir(math) if callable(getattr(math, name))} + # safe_locals.update({'abs': abs}) # Example + safe_locals = {'abs': abs} + + + def evaluate_side(side_str: str, true_branch_currents: List[Optional[float]]) -> Optional[float]: + # print(f"[DEBUG evaluate_side] Evaluating side: '{side_str}'") + substituted_side_str = side_str.strip() + + # 自动补全省略的乘号,例如 10 I_2 -> 10*I_2,I_1 I_2 -> I_1*I_2 + # 1. 数字和变量之间 + substituted_side_str = re.sub(r'(\d)\s*([A-Za-z_][A-Za-z0-9_]*)', r'\1*\2', substituted_side_str) + # 2. 变量和变量之间 + substituted_side_str = re.sub(r'(I_\d+)\s+(I_\d+)', r'\1*\2', substituted_side_str) + # 3. 括号和变量之间 (如 )I_2) + substituted_side_str = re.sub(r'(\))\s*([A-Za-z_][A-Za-z0-9_]*)', r'\1*\2', substituted_side_str) + # 4. 变量和括号之间 (如 I_2(3+4)) + substituted_side_str = re.sub(r'([A-Za-z_][A-Za-z0-9_]*)\s*(\()', r'\1*\2', substituted_side_str) + + # Find all I_(\d+) tokens, sort by index (desc) to replace I_10 before I_1 + current_vars = sorted(list(set(re.findall(r'I_(\d+)', substituted_side_str))), key=lambda x: int(x), reverse=True) + # print(f"[DEBUG evaluate_side] Found current variables: {current_vars}") + + for idx_str in current_vars: + current_idx = int(idx_str) + # print(f"[DEBUG evaluate_side] Attempting to substitute I_{current_idx}") + if 0 < current_idx <= len(true_branch_currents): + val = true_branch_currents[current_idx - 1] + if val is None: + print(f"[DEBUG evaluate_side] Current I_{current_idx} value is None. Cannot evaluate.") + return None # Cannot evaluate if a current is None + # Ensure substitution is for the whole variable name, e.g. I_1 not I_10 + original_substituted_side_str = substituted_side_str + substituted_side_str = re.sub(r'\bI_' + idx_str + r'\b', f"({str(val)})", substituted_side_str) + # print(f"[DEBUG evaluate_side] Substituting I_{idx_str} with ({str(val)}). Before: '{original_substituted_side_str}', After: '{substituted_side_str}'") + else: + print(f"[DEBUG evaluate_side] Warning: Current index I_{idx_str} out of bounds for true_branch_currents (len {len(true_branch_currents)})") + return None # Current index out of bounds + + # Check for any remaining alphabetic characters (potential unreplaced variables or forbidden functions) + # Allows 'e' or 'E' for scientific notation. + remaining_vars_match = re.search(r'[a-df-zA-DF-Z]', substituted_side_str) # Check for letters other than e/E + if remaining_vars_match: + # print(f"[DEBUG evaluate_side] Warning: Expression '{substituted_side_str}' contains unhandled variables or functions (e.g., '{remaining_vars_match.group(0)}') after substitution.") + return None + else: + # print(f"[DEBUG evaluate_side] No unhandled variables found in '{substituted_side_str}'.") + pass + + try: + # print(f"[DEBUG evaluate_side] Attempting to eval: '{substituted_side_str}'") + # Evaluate the expression string. + value = eval(substituted_side_str, safe_globals, safe_locals) + # print(f"[DEBUG evaluate_side] Eval result for '{substituted_side_str}': {value}") + return float(value) + except Exception as e: + print(f"[DEBUG evaluate_side] Error evaluating expression side '{substituted_side_str}': {e}") + return None + + lhs_val = evaluate_side(lhs_str, true_branch_currents) + rhs_val = evaluate_side(rhs_str, true_branch_currents) + + # print(f"[DEBUG _parse_and_eval_equation] LHS evaluated value: {lhs_val}, RHS evaluated value: {rhs_val}") + + if lhs_val is not None and rhs_val is not None: + is_close = np.isclose(lhs_val, rhs_val, atol=atol, rtol=rtol) + # print(f"[DEBUG _parse_and_eval_equation] Comparison np.isclose({lhs_val}, {rhs_val}) results in: {is_close}") + return is_close + + print("[DEBUG _parse_and_eval_equation] LHS or RHS evaluation resulted in None. Returning False.") + return False + + @staticmethod + def _apply_implicit_multiplication(expr_str: str) -> str: + """Applies regex for implicit multiplications.""" + # 1. 数字和变量之间 + expr_str = re.sub(r'(\d(?:\.\d*)?(?:[eE][-+]?\d+)?)\s*([A-Za-z_][A-Za-z0-9_]*)', r'\1*\2', expr_str) + # 2. 变量和变量之间 + expr_str = re.sub(r'(I_\d+)\s+(I_\d+)', r'\1*\2', expr_str) # I_1 I_2 -> I_1*I_2 + expr_str = re.sub(r'([A-Za-z_][A-Za-z0-9_]*)\s+(I_\d+)', r'\1*\2', expr_str) # Potentially other vars like V_1 I_2 -> V_1*I_2 + expr_str = re.sub(r'(I_\d+)\s+([A-Za-z_][A-Za-z0-9_]*)', r'\1*\2', expr_str) # I_1 V_2 -> I_1*V_2 + # 3. 括号和变量之间 (如 )I_2 or (expr) I_2 ) + expr_str = re.sub(r'\)\s*([A-Za-z_][A-Za-z0-9_]*)', r')*\1', expr_str) + # 4. 变量和括号之间 (如 I_2(3+4) or I_2 (3+4) ) + expr_str = re.sub(r'([A-Za-z_][A-Za-z0-9_]*)\s*\(', r'\1*(', expr_str) + # 5. 数字和开括号之间 (e.g. 2(I_1+I_2)) + expr_str = re.sub(r'(\d(?:\.\d*)?(?:[eE][-+]?\d+)?)\s*\(', r'\1*(', expr_str) + # 6. 闭括号和数字之间 (e.g. (I_1+I_2)2) + expr_str = re.sub(r'\)\s*(\d(?:\.\d*)?(?:[eE][-+]?\d+)?)', r')*\1', expr_str) + return expr_str + + @staticmethod + def _evaluate_expression_for_coeffs(expr_str: str, current_values: List[float], num_total_currents: int) -> Optional[float]: + # print(f"[DEBUG _evaluate_expression_for_coeffs] Evaluating expr: '{expr_str}' with I_values: {current_values}") + substituted_expr_str = expr_str + + # Apply implicit multiplication rules + substituted_expr_str = Circuitbootcamp._apply_implicit_multiplication(substituted_expr_str) + # print(f"[DEBUG _evaluate_expression_for_coeffs] After implicit multiplication: '{substituted_expr_str}'") + + # Substitute I_k variables from highest index to lowest to avoid issues like I_10 vs I_1 + for i in range(num_total_currents, 0, -1): + val_to_sub = current_values[i-1] + # Wrap in parentheses for safety, especially for negative numbers + substituted_expr_str = re.sub(r'\bI_' + str(i) + r'\b', f"({str(val_to_sub)})", substituted_expr_str) + + # print(f"[DEBUG _evaluate_expression_for_coeffs] After substituting I_k: '{substituted_expr_str}'") + + # Check for any remaining I_k variables (should not happen if all substituted) or other letters + # Allows 'e' or 'E' for scientific notation in numbers. + remaining_vars_match = re.search(r'\bI_\d+\b|[a-df-zA-DF-Z]', substituted_expr_str) + if remaining_vars_match: + print(f"[DEBUG _evaluate_expression_for_coeffs] Warning: Expression '{substituted_expr_str}' contains unhandled variables (e.g., '{remaining_vars_match.group(0)}') after substitution.") + return None + + safe_globals = {"__builtins__": {}} + safe_locals = {'abs': abs} # Add other math functions if needed by equations + + try: + # Evaluate the expression string. + value = eval(substituted_expr_str, safe_globals, safe_locals) + # print(f"[DEBUG _evaluate_expression_for_coeffs] Eval result for '{substituted_expr_str}': {value}") + return float(value) + except Exception as e: + print(f"[DEBUG _evaluate_expression_for_coeffs] Error evaluating expression '{substituted_expr_str}': {e}") + return None + + @staticmethod + def _get_equation_coefficients(eq_str: str, num_branch_currents: int) -> Optional[List[float]]: + # print(f"[DEBUG _get_equation_coefficients] Processing eq: '{eq_str}' for {num_branch_currents} current variables") + if num_branch_currents == 0: # No currents, no variable coefficients + # Try to evaluate the expression directly if it's like "const1 = const2" + if "=" not in eq_str: + print(f"[DEBUG _get_equation_coefficients] No '=' in equation '{eq_str}' with no currents, cannot form const vector.") + return None + lhs_s, rhs_s = eq_str.split("=", 1) + try: + # Apply implicit multiplication for safety, e.g. "2 pi = 6.28" + lhs_s = Circuitbootcamp._apply_implicit_multiplication(lhs_s) + rhs_s = Circuitbootcamp._apply_implicit_multiplication(rhs_s) + + safe_globals = {"__builtins__": {}} + safe_locals = {'abs': abs} # Add other math functions if needed + + lhs_val = float(eval(lhs_s, safe_globals, safe_locals)) + rhs_val = float(eval(rhs_s, safe_globals, safe_locals)) + # constant term for "expr = 0" is "lhs_val - rhs_val" + # print(f"[DEBUG _get_equation_coefficients] Eq with no currents: '{eq_str}', const_term = {lhs_val - rhs_val}") + return [lhs_val - rhs_val] # Just the constant term + except Exception as e: + print(f"[DEBUG _get_equation_coefficients] Could not eval '{eq_str}' as const=const: {e}") + return None + + if "=" not in eq_str: + print(f"[DEBUG _get_equation_coefficients] No '=' found in equation string: '{eq_str}'") + return None + + lhs_str, rhs_str = eq_str.split('=', 1) + # Form the expression string "LHS - (RHS)" which should evaluate to 0 + expression_str = f"({lhs_str.strip()}) - ({rhs_str.strip()})" + # print(f"[DEBUG _get_equation_coefficients] Standardized expr: '{expression_str}'") + + coeffs = [0.0] * (num_branch_currents + 1) # +1 for the constant term + + # Calculate constant term (value of expression when all I_k = 0) + all_currents_zero = [0.0] * num_branch_currents + constant_term = Circuitbootcamp._evaluate_expression_for_coeffs(expression_str, all_currents_zero, num_branch_currents) + if constant_term is None: + print(f"[DEBUG _get_equation_coefficients] Failed to evaluate constant term for: {expression_str}") + return None + coeffs[num_branch_currents] = constant_term + # print(f"[DEBUG _get_equation_coefficients] Constant term = {constant_term}") + + # Calculate coefficient for each I_k + for k_idx in range(num_branch_currents): # k_idx from 0 to num_branch_currents-1 + current_values_Ik_one = [0.0] * num_branch_currents + current_values_Ik_one[k_idx] = 1.0 + + val_Ik_one = Circuitbootcamp._evaluate_expression_for_coeffs(expression_str, current_values_Ik_one, num_branch_currents) + if val_Ik_one is None: + # print(f"[DEBUG _get_equation_coefficients] Failed to evaluate for I_{k_idx+1}=1 for: {expression_str}") + return None + + # Coefficient of I_k is (Value of expr with I_k=1, others=0) - (Value of expr with all I_k=0, i.e. constant_term) + coeffs[k_idx] = val_Ik_one - constant_term + # print(f"[DEBUG _get_equation_coefficients] Coeff for I_{k_idx+1} = {val_Ik_one} - {constant_term} = {coeffs[k_idx]}") + + # print(f"[DEBUG _get_equation_coefficients] Successfully extracted coeffs for '{eq_str}': {coeffs}") + return coeffs + + @staticmethod + def extract_output(output_str: str) -> Tuple[Optional[List[Optional[float]]], Optional[List[Optional[float]]], List[Dict[str, str]]]: + """ + 从模型的输出字符串中提取所有边的电流值、所有节点的电势值以及方程。 + 优先从 markdown 代码块(```...```)中提取 Equations 区块。 + 电流和电势也优先从最后一个 markdown 代码块中的相应区块提取。 + """ + # print(f"\\n[DEBUG extract_output] --- Starting Extraction ---") + temp_output_preview = output_str[:100] if output_str else 'None' + # print(f"[DEBUG extract_output] Input output_str (first 100 chars): '{temp_output_preview}'...") + + if output_str is None: + print("[DEBUG extract_output] output_str is None, returning None, None, []") + return None, None, [] + + output_str = output_str.strip() + # print(f"[DEBUG extract_output] Stripped output_str (first 100 chars): '{output_str[:100]}'...") # Redundant with above + + branch_currents: List[Optional[float]] = [] + node_potentials: List[Optional[float]] = [] + extracted_equations: List[Dict[str, str]] = [] + + # --- Step 1: Attempt to find the last markdown code block --- + last_code_block_content = None + + # ADDED: Primitive checks for "```" + literal_backtick_count = output_str.count("```") + # print(f"[DEBUG extract_output] output_str.count('```'): {literal_backtick_count}") + literal_backtick_matches = list(re.finditer(r"```", output_str)) + # print(f"[DEBUG extract_output] Positions of literal '```' found by re.finditer(r'```', output_str): {[m.start() for m in literal_backtick_matches]}") + + # MODIFIED REGEX for code block matching + code_block_matches = list(re.finditer(r'```((?:.|\n)*?)```', output_str)) + # print(f"[DEBUG extract_output] Number of code blocks found (using new regex): {len(code_block_matches)}") + + if code_block_matches: + # ADDED DEBUG to see all captured blocks if there are few + if len(code_block_matches) < 5: # Print all if not too many + for i, match in enumerate(code_block_matches): + # print(f"[DEBUG extract_output] Code block {i} content (first 200 chars):\n'''{match.group(1).strip()[:200]}...'''") + pass + + last_code_block_content = code_block_matches[-1].group(1).strip() + # print(f"[DEBUG extract_output] LAST code block content (first 500 chars):\n'''{last_code_block_content[:500]}...'''") # MODIFIED DEBUG + else: # ADDED DEBUG + print("[DEBUG extract_output] No markdown code blocks found by re.finditer.") + + # --- Step 2: Extract Equations --- + # Prefer equations from the last code block if available, otherwise search globally. + text_to_search_equations = last_code_block_content if last_code_block_content else output_str + + # If searching in last_code_block_content, ensure we don't re-match the full output_str if no section found in block + # This means the "else" for global search should only trigger if last_code_block_content is None. + + # MODIFIED REGEX for capturing group + equations_section_match_target = re.search(r'Equations?:?\s*((?:.|\n)*?)(?=Currents?:?|Potentials?:?|$)', text_to_search_equations, re.IGNORECASE) + if not equations_section_match_target and last_code_block_content is not None: # Searched in block, not found, try global + # MODIFIED REGEX for capturing group + equations_section_match_target = re.search(r'Equations?:?\s*((?:.|\n)*?)(?=Currents?:?|Potentials?:?|$)', output_str, re.IGNORECASE) + + + if equations_section_match_target: + equations_text = equations_section_match_target.group(1).strip() + # print(f"[DEBUG extract_output] Equations section found. Text:\n'''{equations_text}'''") + # MODIFIED: Use splitlines() for robust line splitting + raw_eq_lines = equations_text.splitlines() + # print(f"[DEBUG extract_output] Number of raw equation lines found: {len(raw_eq_lines)}") # ADDED DEBUG + for i, line in enumerate(raw_eq_lines): + line = line.strip() + # print(f"[DEBUG extract_output] Processing equation line {i+1}/{len(raw_eq_lines)}: '{line}'") # MODIFIED DEBUG + if not line or line == "...": + # print(f"[DEBUG extract_output] Skipping empty or '...' line.") # ADDED DEBUG + continue + + # KCL Match Attempt + kcl_regex = r'KCL\s+at\s+Node\s+\w+:\s*(.*)' + kcl_match = re.match(kcl_regex, line, re.IGNORECASE) + # print(f"[DEBUG extract_output] KCL match for '{line}' using regex '{kcl_regex}': {bool(kcl_match)}") # ADDED DEBUG + if kcl_match: + eq_s = kcl_match.group(1).strip() + eq_s_cleaned = eq_s.split('//')[0].strip() + if eq_s_cleaned and not eq_s_cleaned.startswith(" Global text + + parsed_currents_from_block = False + parsed_potentials_from_block = False + + if last_code_block_content: + # print("[DEBUG extract_output] Attempting to extract Currents and Potentials from LAST CODE BLOCK.") + current_section_text_block = None + potential_section_text_block = None + + # MODIFIED REGEX for capturing group + current_match_block = re.search(r'Currents?:?\s*((?:.|\n)*?)(?=Potentials?:?|$)', last_code_block_content, re.IGNORECASE) + if current_match_block: + current_section_text_block = current_match_block.group(1).strip() + # print(f"[DEBUG extract_output] [BLOCK] Currents section found. Text:\n'''{current_section_text_block}'''") + + temp_currents_map: Dict[int, float] = {} + # MODIFIED: Process line by line + current_lines = current_section_text_block.splitlines() + # print(f"[DEBUG extract_output] [BLOCK] Number of current lines: {len(current_lines)}") + for line_idx, current_line in enumerate(current_lines): + current_line = current_line.strip() + # print(f"[DEBUG extract_output] [BLOCK] Processing current line {line_idx+1}: '{current_line}'") + # Try primary pattern: I_X = VAL A + match_primary = re.match(r'I_(\d+)\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*A', current_line, re.IGNORECASE) + if match_primary: + edge_idx_str, current_str = match_primary.groups() + # print(f"[DEBUG extract_output] [BLOCK] Primary match: idx='{edge_idx_str}', val='{current_str}'") + try: + edge_idx = int(edge_idx_str) - 1 + if edge_idx >= 0: + temp_currents_map[edge_idx] = float(current_str) + # print(f"[DEBUG extract_output] [BLOCK] Parsed current I_{edge_idx+1} = {current_str}") + parsed_currents_from_block = True # Mark success if at least one parsed + except ValueError: + print(f"[DEBUG extract_output] [BLOCK] ValueError parsing current: idx='{edge_idx_str}', val='{current_str}'") + continue # Process next line after try/except for current match_primary + + # Try alternative pattern: Edge X : VAL A or Current X : VAL A + match_alt = re.match(r'(?:Edge|Current)\s+(\d+)\s*:?\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*A', current_line, re.IGNORECASE) + if match_alt: + edge_idx_str, current_str = match_alt.groups() + # print(f"[DEBUG extract_output] [BLOCK] Alt match: idx='{edge_idx_str}', val='{current_str}'") + try: + edge_idx = int(edge_idx_str) - 1 + if edge_idx >= 0: + temp_currents_map[edge_idx] = float(current_str) + # print(f"[DEBUG extract_output] [BLOCK] Parsed current (alt) I_{edge_idx+1} = {current_str}") + parsed_currents_from_block = True # Mark success + except ValueError: + print(f"[DEBUG extract_output] [BLOCK] ValueError parsing current (alt): idx='{edge_idx_str}', val='{current_str}'") + continue # Process next line after try/except for current match_alt + if current_line: # If line is not empty and didn't match + print(f"[DEBUG extract_output] [BLOCK] No current pattern matched for line: '{current_line}'") + + # Removed old re.findall logic for block currents + # print(f"[DEBUG extract_output] [BLOCK] Currents: Primary indexed matches={len(current_matches_primary_block)}, Alt indexed matches={len(current_matches_alt_block)}") + + # for edge_idx_str, current_str in all_current_matches_block: + # ... (old loop removed) + if temp_currents_map: + max_idx = max(temp_currents_map.keys()) + branch_currents = [temp_currents_map.get(i) for i in range(max_idx + 1)] + # parsed_currents_from_block = True # This is now set inside the loop on first success + # else: + # print("[DEBUG extract_output] [BLOCK] No indexed currents found in Currents section of the code block.") + else: + print("[DEBUG extract_output] [BLOCK] Currents section not found in the code block.") + + # MODIFIED REGEX for capturing group + potential_match_block = re.search(r'Potentials?:?\s*((?:.|\n)*?)(?=$)', last_code_block_content, re.IGNORECASE) + if potential_match_block: + potential_section_text_block = potential_match_block.group(1).strip() + # print(f"[DEBUG extract_output] [BLOCK] Potentials section found. Text:\n'''{potential_section_text_block}'''") + + temp_potentials_map: Dict[int, float] = {} + # MODIFIED: Process line by line + potential_lines = potential_section_text_block.splitlines() + # print(f"[DEBUG extract_output] [BLOCK] Number of potential lines: {len(potential_lines)}") + for line_idx, p_line in enumerate(potential_lines): + p_line = p_line.strip() + # print(f"[DEBUG extract_output] [BLOCK] Processing potential line {line_idx+1}: '{p_line}'") + # Try primary pattern: V_X = VAL V + match_primary_pot = re.match(r'V_(\d+)\s*=\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*V', p_line, re.IGNORECASE) + if match_primary_pot: + node_idx_str, potential_str = match_primary_pot.groups() + # print(f"[DEBUG extract_output] [BLOCK] Primary potential match: idx='{node_idx_str}', val='{potential_str}'") + try: + node_idx = int(node_idx_str) + if node_idx >= 0: + temp_potentials_map[node_idx] = float(potential_str) + # print(f"[DEBUG extract_output] [BLOCK] Parsed potential V_{node_idx} = {potential_str}") + parsed_potentials_from_block = True # Mark success + except ValueError: + print(f"[DEBUG extract_output] [BLOCK] ValueError parsing potential: idx='{node_idx_str}', val='{potential_str}'") + continue + + # Try alternative pattern: Node X : VAL V or Potential X : VAL V + match_alt_pot = re.match(r'(?:Node|Potential)\s+(\d+)\s*:?\s*([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*V', p_line, re.IGNORECASE) + if match_alt_pot: + node_idx_str, potential_str = match_alt_pot.groups() + # print(f"[DEBUG extract_output] [BLOCK] Alt potential match: idx='{node_idx_str}', val='{potential_str}'") + try: + node_idx = int(node_idx_str) + if node_idx >= 0: + temp_potentials_map[node_idx] = float(potential_str) + # print(f"[DEBUG extract_output] [BLOCK] Parsed potential (alt) V_{node_idx} = {potential_str}") + parsed_potentials_from_block = True # Mark success + except ValueError: + print(f"[DEBUG extract_output] [BLOCK] ValueError parsing potential (alt): idx='{node_idx_str}', val='{potential_str}'") + continue + if p_line: # If line is not empty and didn't match + # print(f"[DEBUG extract_output] [BLOCK] No potential pattern matched for line: '{p_line}'") + pass + + # Removed old re.findall logic for block potentials + # print(f"[DEBUG extract_output] [BLOCK] Potentials: Primary indexed matches={len(potential_matches_primary_block)}, Alt indexed matches={len(potential_matches_alt_block)}") + + # for node_idx_str, potential_str in all_potential_matches_block: + # ... (old loop removed) + + if temp_potentials_map: + max_idx = max(temp_potentials_map.keys()) + node_potentials = [temp_potentials_map.get(i) for i in range(max_idx + 1)] + # NO FALLBACK TO ALL FLOATS FOR GLOBAL SEARCH EITHER + # Ensure V0 is 0.0 if present (similar logic to block parsing) + if node_potentials and len(node_potentials) > 0: + if 0 in temp_potentials_map and temp_potentials_map[0] == 0.0: + node_potentials[0] = 0.0 + elif 0 not in temp_potentials_map and node_potentials[0] is not None: # If V0 is not 0 and it's the first value + node_potentials[0] = 0.0 + # else if V0 is missing, it's fine, it will be None in the list unless filled by V0=0 from prompt. + else: + print("[DEBUG extract_output] [GLOBAL] No indexed potentials, trying to extract any floats for potentials.") # Old fallback + values_only = re.findall(r'([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*(?:V|Volts?)?', potential_section_text_global) + if values_only: node_potentials = [float(val) for val in values_only] + else: + print("[DEBUG extract_output] [BLOCK] Potentials section not found in the code block.") + + # --- Step 4: Global search if not found or incomplete from code block --- + if not parsed_currents_from_block: + # print("[DEBUG extract_output] Currents not found in code block or parsing failed, trying GLOBAL search.") + current_section_text_global = None + # MODIFIED REGEX for capturing group + current_match_global = re.search(r'Currents?:?\s*((?:.|\n)*?)(?=Potentials?:?|$)', output_str, re.IGNORECASE) + if current_match_global: + current_section_text_global = current_match_global.group(1).strip() + # print(f"[DEBUG extract_output] [GLOBAL] Currents section found. Text (first 100 chars):\n'''{current_section_text_global[:100]}...'''") + + temp_currents_map: Dict[int, float] = {} + current_matches_primary_global = re.findall(r'I_(\\d+)\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*A', current_section_text_global, re.IGNORECASE) + current_matches_alt_global = re.findall(r'(?:Edge|Current)\\s+(\\d+)\\s*:?\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*A', current_section_text_global, re.IGNORECASE) + all_current_matches_global = current_matches_primary_global + current_matches_alt_global + # print(f"[DEBUG extract_output] [GLOBAL] Currents: Primary indexed matches={len(current_matches_primary_global)}, Alt indexed matches={len(current_matches_alt_global)}") + + for edge_idx_str, current_str in all_current_matches_global: + try: + edge_idx = int(edge_idx_str) - 1 + if edge_idx >= 0: temp_currents_map[edge_idx] = float(current_str) + except ValueError: continue + if temp_currents_map: + max_idx = max(temp_currents_map.keys()) + branch_currents = [temp_currents_map.get(i) for i in range(max_idx + 1)] + # NO FALLBACK TO ALL FLOATS FOR GLOBAL SEARCH EITHER - keep it strict + # else: + # print("[DEBUG extract_output] [GLOBAL] No indexed currents, trying to extract any floats for currents.") # Old fallback + # values_only = re.findall(r'([-+]\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*(?:A|Amperes?)?', current_section_text_global) + # if values_only: branch_currents = [float(val) for val in values_only] + else: + print("[DEBUG extract_output] [GLOBAL] Currents section not found.") + + if not parsed_potentials_from_block: + # print("[DEBUG extract_output] Potentials not found in code block or parsing failed, trying GLOBAL search.") + potential_section_text_global = None + # MODIFIED REGEX for capturing group + potential_match_global = re.search(r'Potentials?:?\s*((?:.|\n)*?)(?=$)', output_str, re.IGNORECASE) + if potential_match_global: + potential_section_text_global = potential_match_global.group(1).strip() + print(f"[DEBUG extract_output] [GLOBAL] Potentials section found. Text (first 100 chars):\n'''{potential_section_text_global[:100]}...'''") + + temp_potentials_map: Dict[int, float] = {} + potential_matches_primary_global = re.findall(r'V_(\\d+)\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*V', potential_section_text_global, re.IGNORECASE) + potential_matches_alt_global = re.findall(r'(?:Node|Potential)\\s+(\\d+)\\s*:?\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*V', potential_section_text_global, re.IGNORECASE) + all_potential_matches_global = potential_matches_primary_global + potential_matches_alt_global + print(f"[DEBUG extract_output] [GLOBAL] Potentials: Primary indexed matches={len(potential_matches_primary_global)}, Alt indexed matches={len(potential_matches_alt_global)}") + + for node_idx_str, potential_str in all_potential_matches_global: + try: + node_idx = int(node_idx_str) + if node_idx >= 0: temp_potentials_map[node_idx] = float(potential_str) + except ValueError: continue + if temp_potentials_map: + max_idx = max(temp_potentials_map.keys()) + node_potentials = [temp_potentials_map.get(i) for i in range(max_idx + 1)] + # NO FALLBACK TO ALL FLOATS FOR GLOBAL SEARCH EITHER + # Ensure V0 is 0.0 if present (similar logic to block parsing) + if node_potentials and len(node_potentials) > 0: + if 0 in temp_potentials_map and temp_potentials_map[0] == 0.0: + node_potentials[0] = 0.0 + elif 0 not in temp_potentials_map and node_potentials[0] is not None: # If V0 is not 0 and it's the first value + node_potentials[0] = 0.0 + # else if V0 is missing, it's fine, it will be None in the list unless filled by V0=0 from prompt. + else: + print("[DEBUG extract_output] [GLOBAL] No indexed potentials, trying to extract any floats for potentials.") # Old fallback + values_only = re.findall(r'([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)\s*(?:V|Volts?)?', potential_section_text_global) + if values_only: node_potentials = [float(val) for val in values_only] + else: + print("[DEBUG extract_output] [GLOBAL] Potentials section not found.") + + # Fallback if sections are not clearly marked and no values extracted yet (original fallback, more constrained now) + if not branch_currents and not node_potentials and not extracted_equations: + print("[DEBUG extract_output] Entering fallback for currents/potentials as NO sections found AND no equations extracted.") + # This fallback should be very conservative, only matching strict I_X = VAL A or V_X = VAL V patterns globally + current_pattern_fallback = r'I_(\\d+)\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*A' + potential_pattern_fallback = r'V_(\\d+)\\s*=\\s*([-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?)\\s*V' + + temp_currents_map_fb: Dict[int, float] = {} + matches_curr_fb = list(re.finditer(current_pattern_fallback, output_str, re.IGNORECASE)) + if matches_curr_fb: print(f"[DEBUG extract_output] Fallback current indexed matches found: {len(matches_curr_fb)}") + for match in matches_curr_fb: + try: + idx = int(match.group(1)) -1 + val = float(match.group(2)) + if idx >=0: temp_currents_map_fb[idx] = val + except ValueError: continue + if temp_currents_map_fb: + max_idx = max(temp_currents_map_fb.keys()) + branch_currents = [temp_currents_map_fb.get(i) for i in range(max_idx + 1)] + + temp_potentials_map_fb: Dict[int, float] = {} + matches_pot_fb = list(re.finditer(potential_pattern_fallback, output_str, re.IGNORECASE)) + if matches_pot_fb: print(f"[DEBUG extract_output] Fallback potential indexed matches found: {len(matches_pot_fb)}") + for match in matches_pot_fb: + try: + idx = int(match.group(1)) + val = float(match.group(2)) + if idx >= 0: temp_potentials_map_fb[idx] = val + except ValueError: continue + if temp_potentials_map_fb: + max_idx = max(temp_potentials_map_fb.keys()) + node_potentials = [temp_potentials_map_fb.get(i) for i in range(max_idx + 1)] + + if branch_currents or node_potentials: + print("[DEBUG extract_output] Returning from fallback with some strictly indexed currents/potentials.") + else: + print("[DEBUG extract_output] Fallback did not find any strictly indexed currents/potentials.") + + + # Final V0=0.0 assurance if potentials were found by any means. + if node_potentials and len(node_potentials) > 0: + # Check if V0 (index 0) exists and is 0. If it exists and not 0, force to 0. + # If it doesn't exist (list is shorter or starts with None at index 0), + # and other potentials exist, we might need to be careful. + # The prompt asks for V0=0V. + # If node_potentials[0] is None or not 0.0, but the list is not empty. + # For now, if node_potentials list exists and has at least one element, ensure node_potentials[0] = 0.0 + # This assumes that if any potentials are given, V_0 is either explicitly given as 0 or implied. + # A more robust way is to check if 0 was in temp_potentials_map and was 0. + # Let's refine: if 0 key exists in any temp_potentials_map and is not 0, set it to 0. + # If 0 key doesn't exist but list is populated, this is ambiguous. + # For now: if potentials are extracted, and node_potentials[0] is present, it MUST be 0. + # If it's not present as first element, it means V_0 was not given or list is malformed. + # The safest is to rely on an explicit V_0 = 0V being parsed, or ensuring the list starts with 0. + # if it starts at all. + # The current logic for setting V0=0 during block/global parsing handles if it's found. + # This final check ensures if a list was somehow formed without V0=0 as the first element, we try to fix it. + if node_potentials[0] is None: # If V0 is explicitly None in a list e.g. [None, 10.0, 5.0] + node_potentials[0] = 0.0 + elif node_potentials[0] != 0.0: # If V0 is some other number + node_potentials[0] = 0.0 + # Removed the problematic elif block that referenced an undefined variable 'expected_potentials_exist' + + + final_branch_currents = branch_currents if branch_currents else None + final_node_potentials = node_potentials if node_potentials else None + + # print(f"[DEBUG extract_output] Final extracted currents: {final_branch_currents}") + # print(f"[DEBUG extract_output] Final extracted potentials: {final_node_potentials}") + # print(f"[DEBUG extract_output] Final extracted equations: {extracted_equations}") + # print(f"[DEBUG extract_output] --- Ending Extraction ---") + return final_branch_currents, final_node_potentials, extracted_equations + + @staticmethod + def _normalize_solution(solution): + """ + The solution is expected to be a float or None after extract_output. + No further normalization usually needed. + """ + return solution + + @classmethod + def _verify_correction(cls, solution, identity: dict) -> bool: + """ + Verifies if the extracted solution (current) matches the pre-calculated expected current. + (This method seems specific to a single value, not directly used by verify_score for lists) + """ + expected_current = identity.get('expected_current') # Assuming 'expected_current' is a single float + + if solution is None and expected_current is None: + return True + if solution is None or expected_current is None: + return False + + return np.isclose(solution, expected_current, atol=1e-2, rtol=1e-3) + + @classmethod + def verify_score(cls, model_output: str, identity: dict, + score_max: float = 1.0, + score_min: float = 0.0, + atol: float = 1e-3, + rtol: float = 1e-3, + equation_reward_weight: float = 1.0, # Changed default to 1.0 + format_score: Optional[float] = None, # Compatibility, unused + w_num: float = 0.2, # Weight for equation number score (将被新逻辑忽略) + w_combined: float = 0.8, # Weight for combined equation correctness and independence score (将被新逻辑忽略) + short_penalty: bool = False, # Added for compatibility + format_penalty: bool = False, # Added for compatibility + **kwargs # Accept any additional keyword arguments + ) -> float: + """ + Verifies model output for currents, potentials, and equations, calculating a comprehensive score. + 新的分数计算逻辑: + 0.5 * (正确的KCL方程数/理应有的KCL方程数[节点数-1]) + + 0.5 * (正确的KVL方程数/理应有的KVL方程数[边数-节点数+1]) + - (不独立的方程数[方程数-系数矩阵的秩] / (理应有的KCL方程数+理应有的KVL方程数)) + """ + # print(f"\\n[DEBUG verify_score] --- Starting Verification ---") + # print(f"[DEBUG verify_score] model_output (first 300 chars):\n'''{model_output[:300]}...'''") + # print(f"[DEBUG verify_score] identity: {identity}") # Can be verbose + + if model_output is None or not model_output.strip(): + print(f"[DEBUG verify_score] Model output is None or empty. Returning score_min: {score_min}") + return score_min + + if not (0 <= equation_reward_weight <= 1.0): + print(f"[DEBUG verify_score] Invalid equation_reward_weight: {equation_reward_weight}. Using 1.0 as default.") + equation_reward_weight = 1.0 + + extracted_currents, extracted_potentials, extracted_equations = cls.extract_output(model_output) + # print(f"[DEBUG verify_score] Extracted Currents: {extracted_currents}") + # print(f"[DEBUG verify_score] Extracted Potentials: {extracted_potentials}") + # print(f"[DEBUG verify_score] Extracted Equations: {extracted_equations}") + + # --- Score for Currents and Potentials --- + correct_vars_count = 0 + total_vars_count = 0 + + expected_currents = identity.get('branch_currents') + expected_potentials = identity.get('node_potentials') + # print(f"[DEBUG verify_score] Expected Currents: {expected_currents}") + # print(f"[DEBUG verify_score] Expected Potentials: {expected_potentials}") + + if expected_currents is not None: + num_currents_to_compare = len(expected_currents) + total_vars_count += num_currents_to_compare + # print(f"[DEBUG verify_score] Comparing {num_currents_to_compare} expected currents.") + if extracted_currents is not None and len(extracted_currents) > 0 : + for i in range(num_currents_to_compare): + is_correct = False + if i < len(extracted_currents) and extracted_currents[i] is not None and expected_currents[i] is not None: + if np.isclose(extracted_currents[i], expected_currents[i], atol=atol, rtol=rtol): + correct_vars_count += 1 + is_correct = True + val_extracted = extracted_currents[i] if i < len(extracted_currents) else 'N/A' + print(f"[DEBUG verify_score] Current I_{i+1}: Expected={expected_currents[i]}, Extracted={val_extracted}, Correct={is_correct}") + else: + # print(f"[DEBUG verify_score] Extracted currents are None or empty, all {num_currents_to_compare} expected currents count as incorrect.") + pass + + if expected_potentials is not None: + num_potentials_to_compare = len(expected_potentials) + total_vars_count += num_potentials_to_compare + # print(f"[DEBUG verify_score] Comparing {num_potentials_to_compare} expected potentials.") + if extracted_potentials is not None and len(extracted_potentials) > 0: + for i in range(num_potentials_to_compare): + is_correct = False + # Node 0 potential should be 0 if present + expected_val = 0.0 if i == 0 and expected_potentials[i] is not None else expected_potentials[i] + + if i < len(extracted_potentials) and extracted_potentials[i] is not None and expected_val is not None: + if np.isclose(extracted_potentials[i], expected_val, atol=atol, rtol=rtol): + correct_vars_count += 1 + is_correct = True + val_extracted = extracted_potentials[i] if i < len(extracted_potentials) else 'N/A' + print(f"[DEBUG verify_score] Potential V_{i}: Expected={expected_val}, Extracted={val_extracted}, Correct={is_correct}") + else: + # print(f"[DEBUG verify_score] Extracted potentials are None or empty, all {num_potentials_to_compare} expected potentials count as incorrect.") + pass + current_potential_score_ratio = 0.0 + if total_vars_count > 0: + current_potential_score_ratio = correct_vars_count / total_vars_count + # print(f"[DEBUG verify_score] Correct Vars: {correct_vars_count}, Total Vars: {total_vars_count}, Var Ratio: {current_potential_score_ratio:.4f}") + + # --- 新的方程分数计算逻辑 --- + equation_accuracy_ratio = 0.0 + + n_nodes = identity.get('n_nodes', 0) + n_edges = len(identity.get('edges', [])) # Number of branches/edges + + # num_branch_currents is essentially n_edges for coefficient vector size + num_branch_currents_for_coeffs = n_edges + + exp_kcl_count = max(0, n_nodes - 1) + exp_kvl_count = max(0, n_edges - n_nodes + 1) if n_nodes > 0 else (1 if n_edges > 0 else 0) # KVL for a single edge is V=E + if n_nodes == 1 and n_edges == 0: exp_kvl_count = 0 # Special case: single isolated node + + exp_total_eq = exp_kcl_count + exp_kvl_count + print(f"[DEBUG verify_score] Expected KCLs: {exp_kcl_count}, Expected KVLs: {exp_kvl_count}, Expected Total Eqs: {exp_total_eq}") + + total_submitted_equations = len(extracted_equations) + print(f"[DEBUG verify_score] Total Submitted Equations: {total_submitted_equations}") + + if equation_reward_weight > 0: # Only calculate equation scores if they contribute + # 分别计算正确的KCL和KVL方程数量 + correct_kcl_count = 0 + correct_kvl_count = 0 + matrix_rank = 0 + coefficient_vectors = [] + + # 计算正确的方程数量,分KCL和KVL类型 + if total_submitted_equations > 0 and expected_currents is not None: + for eq_info in extracted_equations: + eq_str = eq_info.get("equation_str") + eq_type = eq_info.get("type") + if eq_str: + is_eq_correct = cls._parse_and_eval_equation(eq_str, expected_currents, atol=atol, rtol=rtol) + if is_eq_correct: + if eq_type == 'kcl': + correct_kcl_count += 1 + elif eq_type == 'kvl': + correct_kvl_count += 1 + print(f"[DEBUG verify_score] Equation Eval '{eq_str}' (Type: {eq_type}): Correct={is_eq_correct}") + + # 计算回路识别分数 + correct_loop_count = 0 + edges = identity.get('edges', []) + + if total_submitted_equations > 0: + for eq_info in extracted_equations: + eq_str = eq_info.get("equation_str") + eq_type = eq_info.get("type") + if eq_str and eq_type == 'kvl': + # 从KVL方程中提取电流变量对应的边索引 + edge_indices = cls._extract_current_variables_from_equation(eq_str) + # 检查这些边是否构成有效回路 + if cls._check_if_edges_form_loop(edge_indices, edges): + correct_loop_count += 1 + print(f"[DEBUG verify_score] KVL equation '{eq_str}' forms valid loop with edges {edge_indices}") + else: + print(f"[DEBUG verify_score] KVL equation '{eq_str}' does NOT form valid loop with edges {edge_indices}") + + # 计算矩阵的秩来确定独立方程数量 + if total_submitted_equations > 0 and num_branch_currents_for_coeffs > 0: + for eq_info in extracted_equations: + eq_str = eq_info.get("equation_str") + if eq_str: + coeffs = cls._get_equation_coefficients(eq_str, num_branch_currents_for_coeffs) + if coeffs and len(coeffs) == num_branch_currents_for_coeffs + 1: + coefficient_vectors.append(coeffs) + else: + print(f"[DEBUG verify_score] Failed to get valid coefficients for eq: '{eq_str}'") + + if coefficient_vectors: + # We are interested in the rank of the variable coefficients part of the matrix + # Each vector in coefficient_vectors is [c1, c2, ..., cN, const_term] + var_coeffs_matrix = np.array([vec[:-1] for vec in coefficient_vectors]) + + if var_coeffs_matrix.size > 0: # Ensure matrix is not empty + # Suppress RankWarning if matrix is ill-conditioned but rank can still be computed + with np.testing.suppress_warnings() as sup: + sup.filter(UserWarning, "Near rank deficient matrix detected.") # For scipy.linalg.rank + # Using numpy.linalg.matrix_rank directly + try: + matrix_rank = np.linalg.matrix_rank(var_coeffs_matrix, tol=1e-6) # Add tolerance + print(f"[DEBUG verify_score] Coefficient Matrix (vars only) for rank check (shape {var_coeffs_matrix.shape}):\n{var_coeffs_matrix}") + print(f"[DEBUG verify_score] Rank of coefficient matrix: {matrix_rank}") + except Exception as e_rank: + print(f"[DEBUG verify_score] Error calculating matrix rank: {e_rank}") + matrix_rank = 0 # Error in rank calculation + + # 计算KCL分数 + kcl_score = 0.0 + if exp_kcl_count > 0: + kcl_score = min(1.0, correct_kcl_count / exp_kcl_count) + else: + # 如果不需要KCL方程,那么这部分得满分 + kcl_score = 1.0 + + # 计算基础KVL分数(方程正确性) + base_kvl_score = 0.0 + if exp_kvl_count > 0: + base_kvl_score = min(1.0, correct_kvl_count / exp_kvl_count) + else: + # 如果不需要KVL方程,那么这部分得满分 + base_kvl_score = 1.0 + + # 计算回路识别分数 + loop_score = 0.0 + if exp_kvl_count > 0: + loop_score = min(1.0, correct_loop_count / exp_kvl_count) + else: + # 如果不需要KVL方程,回路分数也是满分 + loop_score = 1.0 + + # 新的KVL分数:0.3 * 回路分 + 0.7 * 原来的kvl_score + kvl_score = 0.3 * loop_score + 0.7 * base_kvl_score + + # 计算不独立的方程数 + non_independent_equations = total_submitted_equations - matrix_rank + independence_penalty = 0.0 + if exp_total_eq > 0: + independence_penalty = non_independent_equations / exp_total_eq + + # 最终方程分数 + equation_accuracy_ratio = 0.4 * kcl_score + 0.6 * kvl_score - independence_penalty + # 确保分数不小于0 + equation_accuracy_ratio = max(0.0, equation_accuracy_ratio) + + print(f"[DEBUG verify_score] Correct KCL Equations: {correct_kcl_count} / {exp_kcl_count} = {kcl_score:.4f}") + print(f"[DEBUG verify_score] Correct KVL Equations: {correct_kvl_count} / {exp_kvl_count} = {base_kvl_score:.4f}") + print(f"[DEBUG verify_score] Correct Loop Identification: {correct_loop_count} / {exp_kvl_count} = {loop_score:.4f}") + print(f"[DEBUG verify_score] Final KVL Score (0.3*loop + 0.7*base): {kvl_score:.4f}") + print(f"[DEBUG verify_score] Matrix Rank: {matrix_rank}") + print(f"[DEBUG verify_score] Non-independent Equations: {non_independent_equations}") + print(f"[DEBUG verify_score] Independence Penalty: {independence_penalty:.4f}") + print(f"[DEBUG verify_score] Final Equation Accuracy Ratio: {equation_accuracy_ratio:.4f}") + + # --- Combine Overall Scores --- + variables_weight = 1.0 - equation_reward_weight + + combined_correct_ratio = (variables_weight * current_potential_score_ratio + + equation_reward_weight * equation_accuracy_ratio) + print(f"[DEBUG verify_score] Variables Weight: {variables_weight:.2f}, Overall Equation Reward Weight: {equation_reward_weight:.2f}") + print(f"[DEBUG verify_score] Combined Correct Ratio (vars + eq_weighted): {combined_correct_ratio:.4f}") + + # Handle case where nothing was expected and nothing was provided for vars + if total_vars_count == 0 and not (extracted_currents or extracted_potentials): # No vars expected, none given + # If equations were also not expected and not given, this is perfect. + if exp_total_eq == 0 and total_submitted_equations == 0: + print(f"[DEBUG verify_score] No vars or equations expected, none provided. Perfect score contribution from this part.") + pass # current logic for combined_correct_ratio should handle this. + elif total_vars_count == 0 and (extracted_currents or extracted_potentials): # No vars expected, but some given + print(f"[DEBUG verify_score] No vars expected, but some extracted. current_potential_score_ratio is 0/0=nan, setting to 0.") + current_potential_score_ratio = 0.0 # Avoid NaN if total_vars_count is 0 but extracted exist. + # Recalculate combined_correct_ratio + combined_correct_ratio = (variables_weight * current_potential_score_ratio + + equation_reward_weight * equation_accuracy_ratio) + + if total_vars_count == 0 and total_submitted_equations == 0 and not (extracted_currents or extracted_potentials or extracted_equations) : + # This condition implies nothing was extracted. + # If nothing was expected either (total_vars_count ==0 already handled, and exp_total_eq == 0): + if exp_total_eq == 0: # total_vars_count is already 0 + print(f"[DEBUG verify_score] Nothing extracted, nothing expected. Score should be max.") + return score_max # Perfect score if nothing expected and nothing given. + else: # Nothing extracted, but something was expected + print(f"[DEBUG verify_score] Nothing extracted, but something was expected. Returning score_min: {score_min}") + return score_min + + final_score = score_min + combined_correct_ratio * (score_max - score_min) + # Clamp score to [score_min, score_max] + final_score = max(score_min, min(final_score, score_max)) + + print(f"[DEBUG verify_score] Final Score: {final_score:.4f}") + print(f"[DEBUG verify_score] --- Ending Verification ---") + return final_score + + @staticmethod + def _extract_current_variables_from_equation(eq_str: str) -> List[int]: + """ + 从KVL方程中提取电流变量的索引 + 例如:从 "7*I_1 - 2 + 8*I_2 + 10 = 0" 中提取 [0, 1] (对应边0和边1) + 注意:I_1对应边0,I_2对应边1,等等。I_0被认为是无效的 + 返回的是边的索引列表 + """ + import re + # 匹配 I_数字 的模式 + pattern = r'I_(\d+)' + matches = re.findall(pattern, eq_str) + # 转换为整数并减1(因为I_1对应边0),过滤掉I_0 + edge_indices = [] + for match in matches: + current_index = int(match) + if current_index > 0: # 只接受I_1, I_2, I_3... 不接受I_0 + edge_indices.append(current_index - 1) # I_1对应边0,I_2对应边1 + return list(set(edge_indices)) # 去重 + + @staticmethod + def _check_if_edges_form_loop(edge_indices: List[int], edges: List[List]) -> bool: + """ + 检查给定的边索引是否形成一个回路 + 使用图论方法:如果边集合形成连通图且边数等于节点数,则形成回路 + edges格式: [(R, E, u, v), ...] + """ + if len(edge_indices) < 3: # 至少需要3条边才能形成回路 + return False + + # 收集所有涉及的节点 + nodes_in_edges = set() + edge_connections = [] + + for edge_idx in edge_indices: + if edge_idx < len(edges): + edge = edges[edge_idx] + if len(edge) >= 4: # (R, E, u, v)格式 + node1, node2 = edge[2], edge[3] # u, v + nodes_in_edges.add(node1) + nodes_in_edges.add(node2) + edge_connections.append((node1, node2)) + + if len(nodes_in_edges) == 0: + return False + + # 对于回路:边数应该等于节点数 + if len(edge_connections) != len(nodes_in_edges): + return False + + # 检查连通性:使用并查集或DFS + # 这里使用简单的连通性检查 + if len(nodes_in_edges) < 3: # 至少需要3个节点 + return False + + # 构建邻接表 + adj = {node: [] for node in nodes_in_edges} + for node1, node2 in edge_connections: + adj[node1].append(node2) + adj[node2].append(node1) + + # 检查每个节点的度数是否为2(回路的特征) + for node in nodes_in_edges: + if len(adj[node]) != 2: + return False + + # 检查连通性:从任意节点开始DFS,应该能访问所有节点 + start_node = next(iter(nodes_in_edges)) + visited = set() + stack = [start_node] + + while stack: + node = stack.pop() + if node not in visited: + visited.add(node) + for neighbor in adj[node]: + if neighbor not in visited: + stack.append(neighbor) + + # 如果访问的节点数等于总节点数,则连通 + return len(visited) == len(nodes_in_edges) + + @staticmethod + def _save_kvl_equation_to_file(equation_str: str, kvl_file_path: str = "/cpfs01/shared/llm_ddd/yuzijie/new/kvlstore.txt"): + """ + 将提取到的 KVL 方程追加到指定文件中 + Args: + equation_str: KVL 方程字符串 + kvl_file_path: 保存 KVL 方程的文件路径 + """ + try: + import datetime + # 获取当前时间戳 + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # 确保目录存在 + os.makedirs(os.path.dirname(kvl_file_path), exist_ok=True) + + # 追加 KVL 方程到文件 + with open(kvl_file_path, 'a', encoding='utf-8') as f: + f.write(f"[{timestamp}] KVL Equation: {equation_str}\n") + + print(f"[DEBUG] KVL equation saved to {kvl_file_path}: {equation_str}") + except Exception as e: + print(f"[ERROR] Failed to save KVL equation to file: {e}") + +# Example usage in __main__ would need to be updated to test equations: +# 1. Sample outputs in __main__ should include an "Equations:" section. +# 2. test_identity might need to be augmented if ground truth equations were to be directly compared (not needed for substitution method). +# 3. Calls to verify_score could pass different equation_reward_weight. + +if __name__ == '__main__': + bootcamp = Circuitbootcamp(min_nodes=3, max_nodes=4, seed=42) # Use a fixed seed for consistent tests + + # print("--- Test Case Generation and Prompt ---") + # test_case_identity = bootcamp.case_generator() + # print("Generated Case:") + # print(f" Nodes: {test_case_identity['n_nodes']}") + # print(f" Edges: {test_case_identity['edges']}") + # print(f" Branch Currents (True): {test_case_identity['branch_currents']}") + # print(f" Node Potentials (True): {test_case_identity['node_potentials']}") + + # prompt = bootcamp.prompt_func(test_case_identity) + # # print("\\nGenerated Prompt:\\n", prompt) # Can be very long + # print("-" * 30) + + # print("\\n--- Test Output Extraction and Scoring ---") + + # Mock model output - Perfect Match including equations + # For seed 42, n_nodes=3, edges=[(R,E,u,v),...], true_currents, true_potentials + # Example based on a hypothetical run with seed 42: + # Nodes: 3 + # Edges: [(7.0, 2.0, 0, 1), (8.0, -10.0, 1, 2)] + # Branch Currents (True): [-0.5333333333333333, -0.5333333333333333] + # Node Potentials (True): [0.0, -5.733333333333333, -1.4666666666666661] + # KCL at Node 1: I_1 - I_2 = 0 (True: -0.533 - (-0.533) = 0) + # KVL for Loop (0-1-2-0, assuming V_2 -> V_0 is through some implicit path or a more complex KVL): + # Let's simplify KVL: 7*I_1 - 2 + 8*I_2 - (-10) = V_0 - V_2 = 0 - (-1.466) = 1.466 (This is a path voltage, not a loop) + # KVL for Loop 0-1-2 and back to 0 (if node 2 is connected to 0): + # For a simple series: 7*I_1 - 2 + 8*I_2 - (-10) = 0 (if I_1 is current 0->1, I_2 1->2 and ends) + # True KVL for loop 0-1-2-ref (assuming V2 is grounded directly for simple test): + # V0 - V1 + V1 - V2 + V2 - V0 = 0 + # (E1 - R1*I1) + (E2 - R2*I2) + V2 = 0 ... this is getting complex for a simple test string. + # Let's use the model output format: + + # Example: I_1 = -0.533, I_2 = -0.533 + # KCL at Node 1: I_1 - I_2 = 0 => -0.533 - (-0.533) = 0. Correct. + # KVL for Loop 1: 7*I_1 - 2 + 8*I_2 + 10 = 0 => 7*(-0.533) - 2 + 8*(-0.533) + 10 = -3.731 - 2 - 4.264 + 10 = 0.005. Approx Correct. + +# perfect_output_str = """ +# Equations: +# KCL at Node 1: I_1 - I_2 = 0 +# KVL for Loop 1: 7*I_1 - 2 + 8*I_2 + 10 = 0 + +# Currents: +# I_1 = -0.5333 A +# I_2 = -0.5333 A + +# Potentials: +# V_0 = 0.0 V +# V_1 = -5.7333 V +# V_2 = -1.4667 V +# """ + # Note: The above KVL string is based on the true currents for seed 42 and edges above. + # test_case_identity for seed=42 as run locally: + # Nodes: 3 + # Edges: [(7.0, 2.0, 0, 1), (8.0, -10.0, 1, 2)] + # Branch Currents (True): [-0.5333333333333333, -0.5333333333333333] + # Node Potentials (True): [0.0, 5.733333333333333, 1.4666666666666661] -- My local run got positive V with current direction. + # Let's re-verify the true values. + # If I1 flows 0->1, I2 flows 1->2. + # KCL at 1: I1 - I2 = 0 => I1 = I2. + # Loop 0-1-2-ground(implicit from V0=0): (V0-V1) + (V1-V2) + (V2-V0_ref) = 0 + # R1*I1 - E1 + R2*I2 - E2 = 0 (if current defined other way, signs flip) + # From prompt: "positive current is defined to flow from the first node towards the second node" + # Edge 1 (0-1): R1=7, E1=2 (E is positive if source's positive terminal is at node 1) => voltage rise from E1 is from 0 to 1. + # So, for KVL loop 0-1-2-ref-0: + # -V_R1 + E1 -V_R2 + E2 = 0 => -(7*I1) + 2 -(8*I2) + (-10) = 0 + # -7*I1 - 8*I2 - 8 = 0. Since I1=I2: -15*I1 = 8 => I1 = -8/15 = -0.5333 + # V1 = V0 - (-E1 + R1*I1) = 0 - (-2 + 7*(-8/15)) = 0 - (-2 - 56/15) = 2 + 56/15 = (30+56)/15 = 86/15 = 5.7333 + # V2 = V1 - (-E2 + R2*I2) = 86/15 - (-(-10) + 8*(-8/15)) = 86/15 - (10 - 64/15) = 86/15 - ( (150-64)/15 ) = 86/15 - 86/15 = 0. + # Ah, if node 2 is the end and V0 is ref, V2 is not necessarily 0 unless specified. + # The libcircuit.py calculates potentials. Let's use its output directly. + # For seed=42 (min_nodes=3, max_nodes=3 for consistency): + # Nodes: 3, Edges: [(R=approx 7, E=approx 2, 0, 1), (R=approx 8, E=approx -10, 1, 2)] + # True Branch Currents: [-0.5333333333333333, -0.5333333333333333] + # True Node Potentials: [0.0, 5.733333333333333, 1.4666666666666661] + # So, perfect_output_str should reflect these: + # KCL at Node 1: I_1 - I_2 = 0 (Still true: -0.533 - (-0.533) = 0) + # KVL for Loop (path 0-1-2, voltage relative to V0=0): -7*I_1 + 2 -8*I_2 -10 - V_2 = 0 (if V2 is end of path) No, this is not a loop equation. + # A loop equation from libcircuit is usually derived from MNA. + # Let's make the KVL equation simpler for the test: + # For edge 1 (0-1): V1 - V0 = -R1*I1 + E1 => V1 = -7*(-0.5333) + 2 = 3.7331 + 2 = 5.7331. This matches. + # For edge 2 (1-2): V2 - V1 = -R2*I2 + E2 => V2 = V1 -8*(-0.5333) -10 = 5.7333 + 4.2664 - 10 = -0.0003. This should be 1.466. Calculation diff. + # The E direction: "positive if the source's positive terminal is at node v". So for u-v, it's a rise of E from u to v. + # So, V_v - V_u = R*I + E (if I is u->v and R is resistance drop). Or V_v - V_u = -R*I + E (if R*I is voltage drop). + # Let's use V_drop = R*I. Then V_u - R*I + E = V_v. + # For edge 0-1 (R1,E1,I1): V0 - R1*I1 + E1 = V1 => 0 - 7*(-0.5333) + 2 = V1 => 3.7331 + 2 = 5.7331. This matches. + # For edge 1-2 (R2,E2,I2): V1 - R2*I2 + E2 = V2 => 5.7333 - 8*(-0.5333) + (-10) = V2 => 5.7333 + 4.2664 - 10 = -0.0003. This still doesn't match 1.466. + # The internal solver is correct. The equations written by hand for testing are tricky. + # Let's assume the model provides an equation that SHOULD be true with the true currents. + + # Updated perfect_output_str based on seed 42's true values from a local libcircuit run: + # True Branch Currents: [-0.5333333333333333, -0.5333333333333333] + # True Node Potentials: [0.0, 5.733333333333333, 1.4666666666666661] + # KCL @ Node 1: I_1 - I_2 = 0 + # KVL Example (0-1-2-gnd and then use potentials): V0-R1*I1+E1-R2*I2+E2-V2_true = 0 (if V2_true refers to potential at node 2 relative to gnd) + # (0) - 7*(-0.53333) + (2) - 8*(-0.53333) + (-10) - (1.46666) = 3.73331 + 2 + 4.26664 - 10 - 1.46666 = 10 - 10 - 1.46666 = -1.46666 != 0 + # This is hard. Let's use simpler placeholder equations for testing the eval logic itself. + +# test_identity_main = { +# 'branch_currents': [0.0, 1.7763568394002506e-16, 1.9737298215558337e-16, -3.9474596431116675e-16], +# 'node_potentials': [0.0, 5.0, 18.999999999999996, 14.999999999999998, 17.0], +# 'n_nodes': 5, +# 'edges': [[1, 5, 0, 1], [10, 10, 1, 3], [9, 4, 3, 2], [9, -2, 2, 4]] # Not strictly needed for equation eval if coeffs are in string +# } + +# perfect_output_str_eq_test = """ +# Let's solve the circuit step by step.\n\n### Step 1: Analyze the Circuit Structure\nThe circuit has 5 nodes (0, 1, 2, 3, 4) and 4 edges with resistors and voltage sources. We need to identify the independent loops and nodes.\n\n### Step 2: Formulate Equations using Kirchhoff's Laws\n\n#### Assign Branch Currents\nLet's assign the following branch currents:\n- \\( I_1 \\) for Edge 1 (0-1)\n- \\( I_2 \\) for Edge 2 (1-3)\n- \\( I_3 \\) for Edge 3 (3-2)\n- \\( I_4 \\) for Edge 4 (2-4)\n\n#### Kirchhoff's Current Law (KCL)\nWe need to apply KCL at nodes 1, 2, and 3 (since node 0 is the reference node and has no current flowing into or out of it).\n\n**KCL at Node 1:**\n\\[ I_1 = I_2 + I_5 \\]\nwhere \\( I_5 \\) is the current through the path from node 1 to node 4 (which we need to find).\n\n**KCL at Node 2:**\n\\[ I_5 = I_3 + I_4 \\]\n\n**KCL at Node 3:**\n\\[ I_2 = I_3 \\]\n\n#### Kirchhoff's Voltage Law (KVL)\nWe need to apply KVL around the independent loops. Let's define two loops:\n- Loop 1: 0-1-3-2-4\n- Loop 2: 0-1-3-2\n\n**KVL for Loop 1:**\n\\[ E_1 - R_1 I_1 + E_2 - R_2 I_2 + R_3 I_3 + R_4 I_4 = 0 \\]\n\\[ 5 - 1 I_1 + 10 - 10 I_2 + 9 I_3 + 9 I_4 = 0 \\]\n\\[ 15 - I_1 - 10 I_2 + 9 I_3 + 9 I_4 = 0 \\]\n\n**KVL for Loop 2:**\n\\[ E_1 - R_1 I_1 + E_2 - R_2 I_2 + R_3 I_3 = 0 \\]\n\\[ 5 - 1 I_1 + 10 - 10 I_2 + 9 I_3 = 0 \\]\n\\[ 15 - I_1 - 10 I_2 + 9 I_3 = 0 \\]\n\n### Step 3: Solve the System of Equations\n\nWe have the following system of equations:\n1. \\( I_1 = I_2 + I_5 \\)\n2. \\( I_5 = I_3 + I_4 \\)\n3. \\( I_2 = I_3 \\)\n4. \\( 15 - I_1 - 10 I_2 + 9 I_3 + 9 I_4 = 0 \\)\n5. \\( 15 - I_1 - 10 I_2 + 9 I_3 = 0 \\)\n\nFrom equations 2 and 3:\n\\[ I_5 = I_3 + I_4 \\]\n\\[ I_2 = I_3 \\]\n\nSubstitute \\( I_2 = I_3 \\) into the other equations:\n1. \\( I_1 = I_3 + I_5 \\)\n2. \\( I_5 = I_3 + I_4 \\)\n3. \\( 15 - I_1 - I_3 + 9 I_4 = 0 \\)\n4. \\( 15 - I_1 - I_3 = 0 \\)\n\nSimplify the equations:\n1. \\( I_1 = I_3 + I_5 \\)\n2. \\( I_5 = I_3 + I_4 \\)\n3. \\( 15 - I_1 - I_3 + 9 I_4 = 0 \\)\n4. \\( 15 - I_1 - I_3 = 0 \\)\n\nFrom equation 4:\n\\[ I_1 = 15 - I_3 \\]\n\nSubstitute \\( I_1 = 15 - I_3 \\) into equation 3:\n\\[ 15 - (15 - I_3) - I_3 + 9 I_4 = 0 \\]\n\\[ 0 + 9 I_4 = 0 \\]\n\\[ I_4 = 0 \\]\n\nNow substitute \\( I_4 = 0 \\) into equation 2:\n\\[ I_5 = I_3 + 0 \\]\n\\[ I_5 = I_3 \\]\n\nSubstitute \\( I_5 = I_3 \\) into equation 1:\n\\[ I_1 = I_3 + I_3 \\]\n\\[ I_1 = 2 I_3 \\]\n\nFrom equation 4:\n\\[ I_1 = 15 - I_3 \\]\n\\[ 2 I_3 = 15 - I_3 \\]\n\\[ 3 I_3 = 15 \\]\n\\[ I_3 = 5 \\]\n\nNow we can find the other currents:\n\\[ I_1 = 2 I_3 = 2 \\times 5 = 10 \\]\n\\[ I_2 = I_3 = 5 \\]\n\\[ I_5 = I_3 = 5 \\]\n\n### Step 4: Calculate Node Potentials\n\nUsing the reference node 0 (V_0 = 0 V):\n- Node 1: \\( V_1 = E_1 - R_1 I_1 = 5 - 1 \\times 10 = -5 \\) V\n- Node 2: \\( V_2 = V_1 - R_3 I_3 = -5 - 9 \\times 5 = -50 \\) V\n- Node 3: \\( V_3 = V_2 + E_2 - R_2 I_2 = -50 + 10 - 10 \\times 5 = -100 \\) V\n- Node 4: \\( V_4 = V_2 + R_4 I_4 = -50 + 9 \\times 0 = -50 \\) V\n\n### Final Answers\n\n```\nEquations:\nKCL at Node 1: I_1 = I_2 + I_5\nKCL at Node 2: I_5 = I_3 + I_4\nKCL at Node 3: I_2 = I_3\nKVL for Loop 1: 15 - I_1 - 10 I_2 + 9 I_3 + 9 I_4 = 0\nKVL for Loop 2: 15 - I_1 - 10 I_2 + 9 I_3 = 0\n\nCurrents:\nI_1 = 10 A\nI_2 = 5 A\nI_3 = 5 A\nI_4 = 0 A\n\nPotentials:\nV_0 = 0 V\nV_1 = -5 V\nV_2 = -50 V\nV_3 = -100 V\nV_4 = -50 V\n```\n\nThese are the currents and potentials for the given circuit. +# """ + + + # test_identity_main = {"n_nodes": 6, "edges": [[4, -7, 5, 0], [8, -3, 0, 2], [6, 6, 2, 4], [5, 3, 4, 1], [1, 8, 1, 3], [1, -2, 4, 5], [9, 8, 5, 2]], "branch_currents": [-0.7058823529411766, -0.7058823529411764, 0.35294117647058826, -1.0658141036401502e-15, 0.0, 0.35294117647058876, 1.0588235294117647], "node_potentials": [0.0, 9.529411764705888, 2.6470588235294117, 17.529411764705888, 6.529411764705882, 4.1764705882352935]} + test_identity_main = {"n_nodes": 6, "edges": [[5, -10, 4, 2], [1, 8, 2, 3], [1, 10, 3, 5], [7, 7, 5, 0], [7, 1, 0, 1], [1, 8, 5, 1], [6, 0, 0, 4]], "branch_currents": [0.8964143426294833, 0.8964143426294786, 0.8964143426294804, 0.4780876494023896, -0.4183266932270909, 0.41832669322709126, 0.8964143426294827], "node_potentials": [0.0, 3.928286852589636, -19.860557768924313, -12.756972111553791, -5.3784860557768965, -3.6533864541832726]} + +# perfect_output_str_eq_test = """ +# Let's solve the circuit step by step.\n\n### Step 1: Analyze the Circuit Structure\nThe circuit has 6 nodes (0 to 5) and 7 edges. We need to identify the independent loops and nodes.\n\n### Step 2: Formulate Equations using Kirchhoff's Laws with Branch Currents as Unknowns\n\n#### Assign Branch Currents\nLet's assign branch currents as follows:\n- \\( I_1 \\) for Edge 1 (5-0)\n- \\( I_2 \\) for Edge 2 (0-2)\n- \\( I_3 \\) for Edge 3 (2-4)\n- \\( I_4 \\) for Edge 4 (4-1)\n- \\( I_5 \\) for Edge 5 (1-3)\n- \\( I_6 \\) for Edge 6 (4-5)\n- \\( I_7 \\) for Edge 7 (5-2)\n\n#### Kirchhoff's Current Law (KCL)\nWe need to write KCL equations at nodes 1, 2, 3, 4, and 5 (since node 0 is the reference node with \\( V_0 = 0 \\) V).\n\n**KCL at Node 1:**\n\\[ I_2 + I_4 - I_5 = 0 \\]\n\n**KCL at Node 2:**\n\\[ I_1 + I_7 - I_3 = 0 \\]\n\n**KCL at Node 3:**\n\\[ I_5 - I_7 = 0 \\]\n\n**KCL at Node 4:**\n\\[ I_3 + I_6 - I_4 = 0 \\]\n\n**KCL at Node 5:**\n\\[ I_1 - I_6 - I_7 = 0 \\]\n\n#### Kirchhoff's Voltage Law (KVL)\nWe need to write KVL equations for the independent loops. We can choose loops 1, 2, and 3 as independent loops.\n\n**KVL for Loop 1 (5-0-2-4-5):**\n\\[ -7.00 + 4.00I_1 + 6.00I_3 + 1.00I_6 + 8.00I_7 = 0 \\]\n\n**KVL for Loop 2 (0-2-4-1-0):**\n\\[ 3.00 - 8.00I_2 + 6.00I_3 + 5.00I_4 - 1.00I_5 = 0 \\]\n\n**KVL for Loop 3 (1-3-5-2-1):**\n\\[ 8.00 - 1.00I_5 + 8.00I_7 - 9.00I_2 + 1.00I_6 = 0 \\]\n\n### Step 3: Solve the System of Equations\n\nWe have the following system of linear equations:\n\n1. \\( I_2 + I_4 - I_5 = 0 \\)\n2. \\( I_1 + I_7 - I_3 = 0 \\)\n3. \\( I_5 - I_7 = 0 \\)\n4. \\( I_3 + I_6 - I_4 = 0 \\)\n5. \\( I_1 - I_6 - I_7 = 0 \\)\n6. \\( -7.00 + 4.00I_1 + 6.00I_3 + 1.00I_6 + 8.00I_7 = 0 \\)\n7. \\( 3.00 - 8.00I_2 + 6.00I_3 + 5.00I_4 - 1.00I_5 = 0 \\)\n8. \\( 8.00 - 1.00I_5 + 8.00I_7 - 9.00I_2 + 1.00I_6 = 0 \\)\n\nFrom equations 3 and 5, we can express \\( I_5 \\) and \\( I_7 \\) in terms of \\( I_1 \\) and \\( I_2 \\):\n\n\\[ I_5 = I_7 \\]\n\\[ I_1 = I_6 + I_7 \\]\n\nSubstitute \\( I_5 = I_7 \\) and \\( I_1 = I_6 + I_7 \\) into the other equations:\n\n1. \\( I_2 + I_4 - I_7 = 0 \\)\n2. \\( (I_6 + I_7) + I_7 - I_3 = 0 \\)\n3. \\( I_3 + I_6 - I_4 = 0 \\)\n4. \\( (I_6 + I_7) - I_6 - I_7 = 0 \\) (This simplifies to 0 = 0, which is always true and doesn't provide new information)\n5. \\( -7.00 + 5.00I_6 + 14.00I_7 + 6.00I_3 = 0 \\)\n6. \\( 3.00 - 8.00I_2 + 6.00I_3 + 5.00I_4 - I_7 = 0 \\)\n7. \\( 8.00 - I_7 + 8.00I_7 - 9.00I_2 + I_6 = 0 \\)\n\nSimplify and solve the system:\n\n1. \\( I_2 + I_4 - I_7 = 0 \\)\n2. \\( I_6 + 2I_7 - I_3 = 0 \\)\n3. \\( I_3 + I_6 - I_4 = 0 \\)\n4. \\( -7.00 + 5.00I_6 + 14.00I_7 + 6.00I_3 = 0 \\)\n5. \\( 3.00 - 8.00I_2 + 6.00I_3 + 5.00I_4 - I_7 = 0 \\)\n6. \\( 8.00 - I_7 + 8.00I_7 - 9.00I_2 + I_6 = 0 \\)\n\nSolving these equations, we get:\n\n\\[ I_1 = 1.00 \\, \\text{A} \\]\n\\[ I_2 = 0.50 \\, \\text{A} \\]\n\\[ I_3 = 1.50 \\, \\text{A} \\]\n\\[ I_4 = 0.50 \\, \\text{A} \\]\n\\[ I_5 = 1.50 \\, \\text{A} \\]\n\\[ I_6 = 0.50 \\, \\text{A} \\]\n\\[ I_7 = 1.50 \\, \\text{A} \\]\n\n### Step 4: Calculate Node Potentials\n\nUsing the branch currents, we can calculate the node potentials:\n\n**Node 1:**\n\\[ V_1 = V_0 + I_2 \\cdot R_2 + I_5 \\cdot R_5 = 0 + 0.50 \\cdot 8.00 + 1.50 \\cdot 1.00 = 4.50 \\, \\text{V} \\]\n\n**Node 2:**\n\\[ V_2 = V_0 + I_1 \\cdot R_1 + I_7 \\cdot R_7 = 0 + 1.00 \\cdot 4.00 + 1.50 \\cdot 9.00 = 19.50 \\, \\text{V} \\]\n\n**Node 3:**\n\\[ V_3 = V_0 + I_5 \\cdot R_5 + I_7 \\cdot R_7 = 0 + 1.50 \\cdot 1.00 + 1.50 \\cdot 8.00 = 13.50 \\, \\text{V} \\]\n\n**Node 4:**\n\\[ V_4 = V_0 + I_3 \\cdot R_3 + I_6 \\cdot R_6 = 0 + 1.50 \\cdot 6.00 + 0.50 \\cdot 5.00 = 10.50 \\, \\text{V} \\]\n\n**Node 5:**\n\\[ V_5 = V_0 + I_1 \\cdot R_1 + I_6 \\cdot R_6 + I_7 \\cdot R_7 = 0 + 1.00 \\cdot 4.00 + 0.50 \\cdot 1.00 + 1.50 \\cdot 8.00 = 19.50 \\, \\text{V} \\]\n\n### Step 5: Output the Final Answers\n\n```\nEquations:\nKCL at Node 0: I_2 + I_4 - I_5 = 0\nKCL at Node 1: I_2 + I_4 - I_5 = 0\nKCL at Node 2: I_1 + I_7 - I_3 = 0\nKCL at Node 3: I_5 - I_7 = 0\nKCL at Node 4: I_3 + I_6 - I_4 = 0\nKCL at Node 5: I_1 - I_6 - I_7 = 0\nKVL for Loop 1: -7.00 + 4.00I_1 + 6.00I_3 + 1.00I_6 + 8.00I_7 = 0\nKVL for Loop 2: 3.00 - 8.00I_2 + 6.00I_3 + 5.00I_4 - 1.00I_5 = 0\nKVL for Loop 3: 8.00 - 1.00I_5 + 8.00I_7 - 9.00I_2 + 1.00I_6 = 0\n\nCurrents:\nI_1 = 1.00 A\nI_2 = 0.50 A\nI_3 = 1.50 A\nI_4 = 0.50 A\nI_5 = 1.50 A\nI_6 = 0.50 A\nI_7 = 1.50 A\n\nPotentials:\nV_0 = 0 V\nV_1 = 4.50 V\nV_2 = 19.50 V\nV_3 = 13.50 V\nV_4 = 10.50 V\nV_5 = 19.50 V\n```\n\nThis completes the solution for the given circuit. +# """ +# perfect_output_str_eq_test = """ +# Let's start by analyzing the circuit structure and formulating the equations using Kirchhoff's laws. + +# ### Circuit Structure +# The circuit has 6 nodes (0 to 5) and 7 edges. We need to identify the independent loops and apply Kirchhoff's laws. + +# ### Kirchhoff's Current Law (KCL) +# We will apply KCL at nodes 1, 2, 3, 4, and 5 (since node 0 is the reference node). + +# #### KCL at Node 0: +# \[ I_1 + I_2 = I_7 \] + +# #### KCL at Node 1: +# \[ I_4 = I_5 \] + +# #### KCL at Node 2: +# \[ I_7 - I_2 = I_3 \] + +# #### KCL at Node 3: +# \[ I_5 = 0 \] + +# #### KCL at Node 4: +# \[ I_3 = I_4 + I_6 \] + +# #### KCL at Node 5: +# \[ I_7 + I_6 = I_1 \] + +# ### Kirchhoff's Voltage Law (KVL) +# We will identify the independent loops and apply KVL. + +# #### Loop 1 (5-0-2-5): +# \[ -7 + 4I_1 + 8I_2 - 8I_7 = 0 \] + +# #### Loop 2 (0-2-4-0): +# \[ -3 + 8I_2 - 6I_3 + 3I_4 = 0 \] + +# #### Loop 3 (2-4-5-2): +# \[ 6I_3 + 3I_4 - 1I_6 + 8I_7 - 2 = 0 \] + +# #### Loop 4 (4-1-3-4): +# \[ 3I_4 - 1I_5 + 8 - 2 = 0 \] + +# ### Simplify the Equations +# From KCL at Node 3: +# \[ I_5 = 0 \] + +# This simplifies Loop 4: +# \[ 3I_4 - 2 = 0 \implies I_4 = \frac{2}{3} \text{ A} \] + +# Using \( I_4 = \frac{2}{3} \) in KCL at Node 1: +# \[ I_5 = I_4 = \frac{2}{3} \text{ A} \] + +# Using \( I_4 = \frac{2}{3} \) in KCL at Node 4: +# \[ I_3 = I_4 + I_6 \implies I_3 = \frac{2}{3} + I_6 \] + +# Using \( I_4 = \frac{2}{3} \) in Loop 2: +# \[ -3 + 8I_2 - 6\left(\frac{2}{3} + I_6\right) + 3\left(\frac{2}{3}\right) = 0 \implies -3 + 8I_2 - 4 - 6I_6 + 2 = 0 \implies 8I_2 - 6I_6 = 5 \] + +# Using \( I_4 = \frac{2}{3} \) in Loop 3: +# \[ 6\left(\frac{2}{3} + I_6\right) + 3\left(\frac{2}{3}\right) - I_6 + 8I_7 - 2 = 0 \implies 4 + 6I_6 + 2 - I_6 + 8I_7 - 2 = 0 \implies 4 + 5I_6 + 8I_7 = 0 \implies 5I_6 + 8I_7 = -4 \] + +# From KCL at Node 0: +# \[ I_1 + I_2 = I_7 \implies I_7 = I_1 + I_2 \] + +# From Loop 1: +# \[ -7 + 4I_1 + 8I_2 - 8(I_1 + I_2) = 0 \implies -7 + 4I_1 + 8I_2 - 8I_1 - 8I_2 = 0 \implies -7 - 4I_1 = 0 \implies I_1 = -\frac{7}{4} \text{ A} \] + +# Using \( I_1 = -\frac{7}{4} \) in KCL at Node 0: +# \[ I_7 = -\frac{7}{4} + I_2 \] + +# Using \( I_7 = -\frac{7}{4} + I_2 \) in Loop 3: +# \[ 5I_6 + 8\left(-\frac{7}{4} + I_2\right) = -4 \implies 5I_6 - 14 + 8I_2 = -4 \implies 5I_6 + 8I_2 = 10 \] + +# Solving the system of equations: +# \[ 8I_2 - 6I_6 = 5 \] +# \[ 5I_6 + 8I_2 = 10 \] + +# From these, we get: +# \[ I_6 = -\frac{5}{14} \text{ A}, \quad I_2 = \frac{10}{14} = \frac{5}{7} \text{ A} \] + +# Finally, we can find all currents and voltages. + +# ### Final Answer +# ``` +# Equations: +# KCL at Node 0: I_1 + I_2 = I_7 +# KCL at Node 1: I_4 = I_5 +# KCL at Node 2: I_7 - I_2 = I_3 +# KCL at Node 3: I_5 = 0 +# KCL at Node 4: I_3 = I_4 + I_6 +# KCL at Node 5: I_7 + I_6 = I_1 + +# KVL Loop 1: -I_6*6 + I_5*4 + I_3*3 + I_5*4 + I_4*4 = 0 +# KVL for Loop 2: -3 + 8I_2 - 6I_3 + 3I_4 = 0 +# KVL for Loop 3: 6I_3 + 3I_4 - I_6 + 8I_7 - 2 = 0 +# KVL for Loop 4: 3I_4 - I_5 + 8 - 2 = 0 + +# ``` +# """ + +# perfect_output_str_eq_test = """ +# To solve the given problem, we need to apply Kirchhoff's Current Law (KCL) and Kirchhoff's Voltage Law (KVL) to formulate the necessary equations. Let's follow the step-by-step instructions. + +# ### Step 1: Analyze the Circuit Structure +# The circuit has 4 nodes labeled 0 to 3. The edges and their properties are as follows: +# - Edge 1: R=1.00 Ω, E=-7.00 V, in branch 0-1 (from node 0 to node 1) +# - Edge 2: R=7.00 Ω, E=-2.00 V, in branch 1-2 (from node 1 to node 2) +# - Edge 3: R=4.00 Ω, E=-4.00 V, in branch 2-3 (from node 2 to node 3) +# - Edge 4: R=5.00 Ω, E=-5.00 V, in branch 3-1 (from node 3 to node 1) + +# ### Step 2: Formulate Equations using Kirchhoff's Laws + +# #### KCL Equations: +# - Node 1 (Currents entering and leaving node 1): +# \[ +# I_1 - I_2 - I_4 = 0 +# \] + +# - Node 2 (Currents entering and leaving node 2): +# \[ +# I_2 - I_3 = 0 +# \] + +# #### KVL Equations: +# - Loop 1: Traversing around the loop 0-1-2-3-1 (clockwise): +# \[ +# -E_1 + R_1 \cdot I_1 + E_2 - R_2 \cdot I_2 + E_4 - R_4 \cdot I_4 = 0 +# \] +# Substituting the values: +# \[ +# 7 + 1 \cdot I_1 + 2 - 7 \cdot I_2 - 5 - 5 \cdot I_4 = 0 +# \] +# Simplifying: +# \[ +# 4 + I_1 - 7I_2 - 5I_4 = 0 +# \] + +# - Loop 2: Traversing around the loop 2-3-1-2 (clockwise): +# \[ +# -E_2 + R_2 \cdot I_2 + E_3 - R_3 \cdot I_3 + E_4 - R_4 \cdot I_4 = 0 +# \] +# Substituting the values: +# \[ +# 2 - 7 \cdot I_2 + 4 - 4 \cdot I_3 - 5 - 5 \cdot I_4 = 0 +# \] +# Simplifying: +# \[ +# 1 - 7I_2 - 4I_3 - 5I_4 = 0 +# \] + +# ### Step 3: Output the Equations + +# ``` +# Equations: +# KCL at Node 1: I_1 - I_2 - I_4 = 0 +# KCL at Node 2: I_2 - I_3 = 0 +# KVL for Loop 1: 4 + I_1 - 7I_2 - 5I_4 = 0 +# KVL for Loop 2: 1 - 7I_2 - 4I_3 - 5I_4 = 0 +# ``` + + + + +# These are the equations formulated based on Kirchhoff's Laws for the given circuit. +# """ + +# 偷懒没说kvl 的版本如下 + + perfect_output_str_eq_test = """ +Let's analyze the circuit and then apply Kirchhoff's Laws to formulate the equations. + +### Step 1: Analyze the Circuit Structure + +The circuit has 6 nodes (0, 1, 2, 3, 4, 5) and the following edges: + +- Edge 1: R=5.00 Ohm, E=-10.00 V, from node 4 to node 2 +- Edge 2: R=1.00 Ohm, E=8.00 V, from node 2 to node 3 +- Edge 3: R=1.00 Ohm, E=10.00 V, from node 3 to node 5 +- Edge 4: R=7.00 Ohm, E=7.00 V, from node 5 to node 0 +- Edge 5: R=7.00 Ohm, E=1.00 V, from node 0 to node 1 +- Edge 6: R=1.00 Ohm, E=8.00 V, from node 5 to node 1 +- Edge 7: R=6.00 Ohm, E=0.00 V, from node 0 to node 4 + +### Step 2: Formulate Equations using Kirchhoff's Laws + +#### Assigning Branch Current Variables + +Let: +- \( I_1 \) be the current through Edge 1 (from node 4 to node 2) +- \( I_2 \) be the current through Edge 2 (from node 2 to node 3) +- \( I_3 \) be the current through Edge 3 (from node 3 to node 5) +- \( I_4 \) be the current through Edge 4 (from node 5 to node 0) +- \( I_5 \) be the current through Edge 5 (from node 0 to node 1) +- \( I_6 \) be the current through Edge 6 (from node 5 to node 1) +- \( I_7 \) be the current through Edge 7 (from node 0 to node 4) + +#### Applying Kirchhoff's Current Law (KCL) at each node + +- **Node 0:** + \[ + I_4 - I_5 - I_7 = 0 + \] + +- **Node 1:** + \[ + I_5 + I_6 = 0 + \] + +- **Node 2:** + \[ + I_1 - I_2 = 0 + \] + +- **Node 3:** + \[ + I_2 - I_3 = 0 + \] + +- **Node 4:** + \[ + I_7 - I_1 = 0 + \] + +### Step 3: Output the Equations + +``` +Equations: +KCL at Node 0: I_4 - I_5 - I_7 = 0 +KCL at Node 1: I_5 + I_6 = 0 +KCL at Node 2: I_1 - I_2 = 0 +KCL at Node 3: I_2 - I_3 = 0 +KCL at Node 4: I_7 - I_1 = 0 +KVL for Loop 1: I_5 + I_6 + I_4 = 0 +KVL for Loop 2: I_5 + I_6 = 0 +``` + +These are the equations based on the given circuit and Kirchhoff's Laws. +""" + print(f"Testing with perfect output:") + score = bootcamp.verify_score(perfect_output_str_eq_test, test_identity_main, equation_reward_weight=1) + print(f" Score: {score}") # Expect 1.0 + +# partial_output_str_eq_test = """ +# Equations: +# KCL at Node 1: I_1 + I_2 = 0 // False: 1 + (-2) = -1 != 0 +# KVL for LoopX: 2*I_1 - I_3 + 1.0001 = 0 // Approx True: 2-3+1.0001 = 0.0001 +# Currents: +# I_1 = 1.0 A +# I_2 = -1.999 A # Almost correct for I_2 +# // I_3 missing +# Potentials: +# V_0 = 0.0 V +# V_1 = 5.01 V # Almost correct for V_1 +# // V_2, V_3 missing +# """ +# # Variables: I1 (correct), I2 (correct), I3 (missing, count as 1 var) -> 2/3 correct for currents with values +# # V0 (correct), V1 (correct), V2 (missing), V3 (missing) -> 2/4 correct for potentials +# # Total vars correct: 2+2=4. Total vars expected: 3+4=7. Var_ratio = 4/7 +# # Equations: Eq1 (False), Eq2 (True) -> 1/2 correct. Eq_ratio = 0.5 +# # Score = 0.7 * (4/7) + 0.3 * (0.5) = 0.7 * 0.5714 + 0.3 * 0.5 = 0.4 + 0.15 = 0.55 +# print(f"Testing with partial output (eq_weight=0.3):") +# score = bootcamp.verify_score(partial_output_str_eq_test, test_identity_main, equation_reward_weight=0.3) +# print(f" Score: {score}") # Expect approx 0.55 + +# only_equations_correct_str = """ +# Equations: +# KCL at Node 1: I_1 + I_2 + 1 = 0 +# KVL for LoopX: 2*I_1 - I_3 + 1 = 0 +# Currents: +# I_1 = 100 A +# Potentials: +# V_0 = 0 V +# """ +# # Variables: I1(F), I2(M), I3(M) -> 0/3. V0(T), V1(M), V2(M), V3(M) -> 1/4. Var_ratio = 1/7 +# # Equations: Eq1(T), Eq2(T) -> 2/2 = 1.0. Eq_ratio = 1.0 +# # Score = 0.7 * (1/7) + 0.3 * (1.0) = 0.7 * 0.1428 + 0.3 = 0.09996 + 0.3 = ~0.40 +# print(f"Testing with only equations mostly correct (eq_weight=0.3):") +# score = bootcamp.verify_score(only_equations_correct_str, test_identity_main, equation_reward_weight=0.3) +# print(f" Score: {score}") # Expect approx 0.40 + +# bad_format_output_str = "This is not parseable." +# print(f"Testing with bad format (eq_weight=0.3):") +# score = bootcamp.verify_score(bad_format_output_str, test_identity_main, equation_reward_weight=0.3) +# print(f" Score: {score}") # Expect 0.0 (score_min) + + print("\\nCircuit bootcamp tests with equation scoring complete.") \ No newline at end of file diff --git a/internbootcamp/libs/circuit/libcircuit.py b/internbootcamp/libs/circuit/libcircuit.py new file mode 100644 index 0000000..ad3fbcb --- /dev/null +++ b/internbootcamp/libs/circuit/libcircuit.py @@ -0,0 +1,211 @@ +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