From 2f2437986608ebb9391b30f2a1050cd609a99cd5 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 16:37:19 -0800 Subject: [PATCH 01/73] add logic to print out circuit info --- src/skidl/__init__.py | 1 + src/skidl/circuit.py | 97 +++++++++++++++++++++++++++++++++++++++++++ src/skidl/skidl.py | 2 + 3 files changed, 100 insertions(+) diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index 544a8e7e8..b2bb1cc7a 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -57,6 +57,7 @@ generate_svg, generate_xml, lib_search_paths, + get_circuit_info, no_files, reset, get_default_tool, diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 076bc9a6a..25b1eb12e 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1170,3 +1170,100 @@ def no_files(self, stop): """Don't output any files if stop is True.""" self._no_files = stop stop_log_file_output(stop) + + + def get_circuit_info(self, filename="circuit_description.txt"): + """ + Save circuit information to a text file and return the description as a string. + Shows hierarchical structure of the circuit with consolidated parts and connections. + """ + circuit_info = [] + circuit_info.append("Circuit Description:") + circuit_info.append("=" * 40) + circuit_info.append(f"Circuit Name: {self.name}") + circuit_info.append(f"Top Level Hierarchy: {self.hierarchy}") + circuit_info.append("\nHierarchy Details:") + circuit_info.append("-" * 40) + + # Collect all hierarchies + hierarchies = set() + for part in self.parts: + hierarchies.add(part.hierarchy) + + # Group parts by hierarchy + hierarchy_parts = {} + for part in self.parts: + if part.hierarchy not in hierarchy_parts: + hierarchy_parts[part.hierarchy] = [] + hierarchy_parts[part.hierarchy].append(part) + + # Get nets and group by hierarchy + distinct_nets = self.get_nets() + net_hierarchies = {} + for net in distinct_nets: + net_hier_connections = {} + for pin in net.pins: + hier = pin.part.hierarchy + if hier not in net_hier_connections: + net_hier_connections[hier] = [] + net_hier_connections[hier].append(pin) + + for hier in net_hier_connections: + if hier not in net_hierarchies: + net_hierarchies[hier] = [] + net_hierarchies[hier].append((net, net_hier_connections)) + + # Print consolidated information for each hierarchy level + first_hierarchy = True + for hier in sorted(hierarchies): + if not first_hierarchy: + circuit_info.append("_" * 53) # Separator line between hierarchies + else: + first_hierarchy = False + + circuit_info.append(f"Hierarchy Level: {hier}") + + # Parts in this hierarchy + if hier in hierarchy_parts: + circuit_info.append("Parts:") + for part in sorted(hierarchy_parts[hier], key=lambda p: p.ref): + circuit_info.append(f" Part: {part.ref}") + circuit_info.append(f" Name: {part.name}") + circuit_info.append(f" Value: {part.value}") + circuit_info.append(f" Footprint: {part.footprint}") + circuit_info.append(" Pins:") + for pin in part.pins: + net_name = pin.net.name if pin.net else "unconnected" + circuit_info.append(f" {pin.num}/{pin.name}: {net_name}") + + # Nets connected to this hierarchy + if hier in net_hierarchies: + circuit_info.append(" Nets:") + for net, hier_connections in sorted(net_hierarchies[hier], key=lambda x: x[0].name): + circuit_info.append(f" Net: {net.name}") + # Local connections + local_pins = hier_connections[hier] + circuit_info.append(" Local Connections:") + for pin in sorted(local_pins, key=lambda p: p.part.ref): + circuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") + + # Cross-hierarchy connections + other_hierarchies = set(hier_connections.keys()) - {hier} + if other_hierarchies: + circuit_info.append(" Connected to Other Hierarchies:") + for other_hier in sorted(other_hierarchies): + circuit_info.append(f" {other_hier}:") + for pin in sorted(hier_connections[other_hier], key=lambda p: p.part.ref): + circuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") + + # Add end marker + circuit_info.append("=" * 15 + " END CIRCUIT " + "=" * 15) + + # Combine into final string + circuit_text = "\n".join(circuit_info) + + # Save to file + with open(filename, 'w') as f: + f.write(circuit_text) + + return circuit_text \ No newline at end of file diff --git a/src/skidl/skidl.py b/src/skidl/skidl.py index fff2109e5..b199dac3e 100644 --- a/src/skidl/skidl.py +++ b/src/skidl/skidl.py @@ -25,6 +25,7 @@ "generate_schematic", "generate_svg", "generate_graph", + "get_circuit_info", "reset", "backup_parts", "empty_footprint_handler", @@ -69,6 +70,7 @@ reset = default_circuit.reset backup_parts = default_circuit.backup_parts no_files = default_circuit.no_files +get_circuit_info = default_circuit.get_circuit_info empty_footprint_handler = default_empty_footprint_handler From 3183cfefa03258ff5fb8d43db09cef03e56040ea Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 16:47:47 -0800 Subject: [PATCH 02/73] add test script --- skidl_test.py | 92 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 skidl_test.py diff --git a/skidl_test.py b/skidl_test.py new file mode 100644 index 000000000..9e202625b --- /dev/null +++ b/skidl_test.py @@ -0,0 +1,92 @@ +from skidl import * + +@subcircuit +def rc_filter(inp, outp, gnd): + """RC low-pass filter""" + r = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='10K') + c = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='0.1uF') + inp += r[1] + r[2] += c[1] + c[2] += gnd + outp += r[2] + +@subcircuit +def input_protection(inp, outp, gnd): + """Input protection and bulk capacitance""" + d_protect = Part("Device", 'D', + footprint='Diode_SMD:D_SOD-123', + value='1N4148W') + c_bulk = Part("Device", 'C', + footprint='Capacitor_THT:CP_Radial_D10.0mm_P5.00mm', + value='100uF') + + inp += d_protect['A'] + outp += d_protect['K'] + inp += c_bulk[1] + gnd += c_bulk[2] + +@subcircuit +def voltage_regulator(inp, outp, gnd): + """5V voltage regulator with decoupling caps""" + reg = Part("Regulator_Linear", "LM7805_TO220", + footprint="Package_TO_SOT_THT:TO-220-3_Vertical") + cin = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='10uF') + cout = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='10uF') + inp += cin[1], reg['VI'] + cin[2] += gnd + reg['GND'] += gnd + reg['VO'] += cout[1], outp + cout[2] += gnd + +@subcircuit +def voltage_divider(inp, outp, gnd): + """Basic voltage divider subcircuit""" + r1 = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='1K') + r2 = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='500') + inp += r1[1] + r1[2] += r2[1] + r2[2] += gnd + outp += r1[2] + +@subcircuit +def output_termination(inp, gnd): + """Output termination resistor""" + r_term = Part("Device", 'R', + footprint='Resistor_SMD.pretty:R_0805_2012Metric', + value='100K') + inp += r_term[1] + gnd += r_term[2] + +@subcircuit +def power_section(raw_in, reg_out, filt_out, gnd): + """Power section with regulation and filtering""" + protected_in = Net() + input_protection(raw_in, protected_in, gnd) + voltage_regulator(protected_in, reg_out, gnd) + rc_filter(reg_out, filt_out, gnd) + +@subcircuit +def double_divider(inp, outp, gnd): + """Two voltage dividers in series with termination""" + mid = Net() + voltage_divider(inp, mid, gnd) + voltage_divider(mid, outp, gnd) + output_termination(outp, gnd) + +@subcircuit +def complete_circuit(): + """Top level circuit connecting all subcircuits""" + vin = Net() + vreg = Net() + vfilt = Net() + vout = Net() + gnd = Net() + + power_section(vin, vreg, vfilt, gnd) + double_divider(vfilt, vout, gnd) + +# Create the complete circuit +complete_circuit() + +# Get circuit info +circuit_description = get_circuit_info() \ No newline at end of file From c3e1459e526cb791d77f65af78f04962b918115f Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 17:10:23 -0800 Subject: [PATCH 03/73] add circuit analyzer function --- src/skidl/__init__.py | 1 + src/skidl/circuit.py | 3 +- src/skidl/circuit_analyzer.py | 103 ++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 src/skidl/circuit_analyzer.py diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index b2bb1cc7a..ed9741373 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -42,6 +42,7 @@ ) from .pin import Pin from .schlib import SchLib, load_backup_lib +from .circuit_analyzer import SkidlCircuitAnalyzer from .skidl import ( ERC, POWER, diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 25b1eb12e..e6f2468ba 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1177,6 +1177,7 @@ def get_circuit_info(self, filename="circuit_description.txt"): Save circuit information to a text file and return the description as a string. Shows hierarchical structure of the circuit with consolidated parts and connections. """ + from .circuit_analyzer import SkidlCircuitAnalyzer circuit_info = [] circuit_info.append("Circuit Description:") circuit_info.append("=" * 40) @@ -1266,4 +1267,4 @@ def get_circuit_info(self, filename="circuit_description.txt"): with open(filename, 'w') as f: f.write(circuit_text) - return circuit_text \ No newline at end of file + return circuit_text diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py new file mode 100644 index 000000000..0205232d5 --- /dev/null +++ b/src/skidl/circuit_analyzer.py @@ -0,0 +1,103 @@ +"""Module for analyzing SKIDL circuits using LLM.""" + +import os +from typing import Dict, Optional +import anthropic + +class SkidlCircuitAnalyzer: + """Analyzes SKIDL circuits using LLM.""" + + def __init__(self, api_key: Optional[str] = None): + self.api_key = api_key or os.getenv('ANTHROPIC_API_KEY') + self.client = anthropic.Client(api_key=self.api_key) + + def _generate_analysis_prompt(self, circuit_description: str) -> str: + """Generate structured prompt for circuit analysis.""" + prompt = f""" +Please analyze this electronic circuit design as an expert electronics engineer. +Focus on practical improvements for reliability, safety, and performance. + +Circuit Description: +{circuit_description} + +Please provide a detailed analysis covering: + +1. Hierarchical Design Review: +- Evaluate the overall hierarchical structure +- Assess modularity and organization +- Identify potential interface issues between hierarchical blocks + +2. Power Distribution Analysis: +- Review power path from input to regulated output +- Evaluate decoupling and filtering strategy +- Check for potential voltage drop issues +- Assess protection mechanisms + +3. Signal Path Analysis: +- Trace critical signal paths +- Evaluate voltage divider networks +- Check for loading effects +- Identify potential noise coupling issues + +4. Component Selection Review: +- Assess component values and ratings +- Review footprint selections +- Check for standard vs specialized parts +- Evaluate thermal considerations + +5. Safety and Reliability: +- Identify potential failure modes +- Review protection mechanisms +- Assess thermal management +- Check compliance with basic safety practices + +6. Specific Recommendations: +- List concrete suggestions for improvements +- Highlight any critical issues that need addressing +- Suggest alternative approaches where relevant +- Provide component value optimizations if needed + +7. Design Best Practices: +- Compare against industry standard practices +- Identify any deviations from typical approaches +- Suggest documentation improvements +- Note any missing test points or debug features + +Please provide detailed technical reasoning for each point and specific recommendations where applicable. +""" + return prompt + + def analyze_circuit(self, circuit_description: str) -> Dict: + """Perform LLM analysis of the circuit.""" + try: + prompt = self._generate_analysis_prompt(circuit_description) + + response = self.client.messages.create( + model="claude-3-sonnet-20240229", + max_tokens=4000, + messages=[{ + "role": "user", + "content": prompt + }] + ) + + return { + "success": True, + "analysis": response.content[0].text, + "timestamp": response.created + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + def save_analysis(self, analysis: Dict, output_file: str = "circuit_analysis.txt"): + """Save the analysis results to a file.""" + if analysis["success"]: + with open(output_file, "w") as f: + f.write(analysis["analysis"]) + else: + with open(output_file, "w") as f: + f.write(f"Analysis failed: {analysis['error']}") From 9f835db855ba8e04dcc938fb474b1ab26b075a1e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 17:29:24 -0800 Subject: [PATCH 04/73] logic to query anthropic added and working --- skidl_test.py | 3 +- src/skidl/__init__.py | 1 + src/skidl/circuit.py | 113 ++++++++++++++++++++++++++++++++++++++++++ src/skidl/skidl.py | 2 + 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/skidl_test.py b/skidl_test.py index 9e202625b..fc7be8995 100644 --- a/skidl_test.py +++ b/skidl_test.py @@ -89,4 +89,5 @@ def complete_circuit(): complete_circuit() # Get circuit info -circuit_description = get_circuit_info() \ No newline at end of file +circuit_description = get_circuit_info() +analyze_with_llm() \ No newline at end of file diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index ed9741373..3f6fb0de3 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -59,6 +59,7 @@ generate_xml, lib_search_paths, get_circuit_info, + analyze_with_llm, no_files, reset, get_default_tool, diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index e6f2468ba..53102b46b 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1268,3 +1268,116 @@ def get_circuit_info(self, filename="circuit_description.txt"): f.write(circuit_text) return circuit_text + + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt"): + """ + Analyze the circuit using LLM (Large Language Model) through the SkidlCircuitAnalyzer. + + This method performs a comprehensive analysis of the circuit using an LLM, + focusing on design review, power distribution, signal paths, component selection, + safety, and best practices. + + Args: + api_key (str, optional): Anthropic API key. If None, will try to use ANTHROPIC_API_KEY + environment variable. Defaults to None. + output_file (str, optional): File to save the analysis results. + Defaults to "circuit_llm_analysis.txt". + + Returns: + dict: Analysis results containing: + - success (bool): Whether analysis was successful + - analysis (str): The detailed analysis text if successful + - error (str): Error message if analysis failed + """ + from datetime import datetime + import time + from .circuit_analyzer import SkidlCircuitAnalyzer + + print("\n=== Starting Circuit Analysis with LLM ===") + start_time = time.time() + + # First get the circuit description + print("\nGenerating circuit description...") + circuit_description = self.get_circuit_info() + print(f"Circuit description generated ({len(circuit_description)} characters)") + + # Initialize the analyzer + print("\nInitializing SkidlCircuitAnalyzer...") + try: + analyzer = SkidlCircuitAnalyzer(api_key=api_key) + print("Analyzer initialized successfully") + except Exception as e: + print(f"Error initializing analyzer: {e}") + raise + + # Perform the analysis + try: + # Create message and get response + print("\nGenerating analysis prompt...") + prompt = analyzer._generate_analysis_prompt(circuit_description) + prompt_tokens = len(prompt.split()) # Rough token count estimate + print(f"Prompt generated ({prompt_tokens} estimated tokens)") + print("\nPrompt preview (first 200 chars):") + print(f"{prompt[:200]}...") + + print("\nSending request to Claude API...") + request_start = time.time() + print("Waiting for response...") + + response = analyzer.client.messages.create( + model="claude-3-sonnet-20240229", + max_tokens=4000, + messages=[{ + "role": "user", + "content": prompt + }] + ) + + request_time = time.time() - request_start + print(f"\nResponse received in {request_time:.2f} seconds") + + # Parse response + print("\nProcessing response...") + analysis_text = response.content[0].text + analysis_tokens = len(analysis_text.split()) # Rough token count estimate + + print(f"Response length: {len(analysis_text)} characters") + print(f"Estimated response tokens: {analysis_tokens}") + + analysis_results = { + "success": True, + "analysis": analysis_text, + "timestamp": int(datetime.now().timestamp()), + "request_time_seconds": request_time, + "prompt_tokens": prompt_tokens, + "response_tokens": analysis_tokens + } + + # Save results to file + if output_file: + print(f"\nSaving analysis to {output_file}...") + with open(output_file, "w") as f: + f.write(analysis_results["analysis"]) + print("Analysis saved successfully") + + total_time = time.time() - start_time + print(f"\n=== Analysis completed in {total_time:.2f} seconds ===") + return analysis_results + + except Exception as e: + print(f"\nERROR: Analysis failed: {str(e)}") + error_results = { + "success": False, + "error": str(e), + "timestamp": int(datetime.now().timestamp()) + } + + # Save error to file + if output_file: + print(f"\nSaving error message to {output_file}...") + with open(output_file, "w") as f: + f.write(f"Analysis failed: {error_results['error']}") + print("Error message saved") + + print("\n=== Analysis failed ===") + return error_results \ No newline at end of file diff --git a/src/skidl/skidl.py b/src/skidl/skidl.py index b199dac3e..b3e87830e 100644 --- a/src/skidl/skidl.py +++ b/src/skidl/skidl.py @@ -26,6 +26,7 @@ "generate_svg", "generate_graph", "get_circuit_info", + "analyze_with_llm", "reset", "backup_parts", "empty_footprint_handler", @@ -71,6 +72,7 @@ backup_parts = default_circuit.backup_parts no_files = default_circuit.no_files get_circuit_info = default_circuit.get_circuit_info +analyze_with_llm = default_circuit.analyze_with_llm empty_footprint_handler = default_empty_footprint_handler From 6c53686a9bbb212466dc857afc59b9ea021412cf Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 17:54:23 -0800 Subject: [PATCH 05/73] add llm analysis class --- src/skidl/__init__.py | 1 + src/skidl/circuit_analyzer.py | 313 ++++++++++++++++++++++++++-------- src/skidl/llm_providers.py | 164 ++++++++++++++++++ 3 files changed, 408 insertions(+), 70 deletions(-) create mode 100644 src/skidl/llm_providers.py diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index 3f6fb0de3..b87be0c20 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -43,6 +43,7 @@ from .pin import Pin from .schlib import SchLib, load_backup_lib from .circuit_analyzer import SkidlCircuitAnalyzer +from .llm_providers import * from .skidl import ( ERC, POWER, diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index 0205232d5..486989957 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -1,103 +1,276 @@ -"""Module for analyzing SKIDL circuits using LLM.""" +"""Module for analyzing SKIDL circuits using various LLM providers.""" -import os -from typing import Dict, Optional -import anthropic +from typing import Dict, Optional, List +from datetime import datetime +import time +from .llm_providers import get_provider class SkidlCircuitAnalyzer: - """Analyzes SKIDL circuits using LLM.""" - - def __init__(self, api_key: Optional[str] = None): - self.api_key = api_key or os.getenv('ANTHROPIC_API_KEY') - self.client = anthropic.Client(api_key=self.api_key) + """Analyzes SKIDL circuits using various LLM providers.""" + def __init__( + self, + provider: str = "anthropic", + api_key: Optional[str] = None, + model: Optional[str] = None, + **kwargs + ): + """ + Initialize the circuit analyzer. + + Args: + provider: LLM provider to use ("anthropic", "openai", or "openrouter") + api_key: API key for the chosen provider + model: Specific model to use (provider-dependent) + **kwargs: Additional provider-specific configuration + """ + self.provider = get_provider(provider, api_key) + self.model = model + self.config = kwargs + def _generate_analysis_prompt(self, circuit_description: str) -> str: """Generate structured prompt for circuit analysis.""" prompt = f""" Please analyze this electronic circuit design as an expert electronics engineer. -Focus on practical improvements for reliability, safety, and performance. +Provide an extremely detailed technical analysis with specific calculations, thorough explanations, +and comprehensive recommendations. Focus on practical improvements for reliability, safety, and performance. Circuit Description: {circuit_description} -Please provide a detailed analysis covering: +Please provide an extensive analysis covering each of these areas in great detail: + +1. Comprehensive Design Architecture Review: +- Evaluate the overall hierarchical structure and design patterns used +- Assess modularity, reusability, and maintainability of each block +- Analyze interface definitions and protocols between blocks +- Review signal flow and control paths +- Evaluate design scalability and future expansion capabilities +- Identify potential bottlenecks in the architecture + +2. In-depth Power Distribution Analysis: +- Map complete power distribution network from input to all loads +- Analyze voltage regulation performance including: + * Load regulation calculations + * Line regulation performance + * Transient response characteristics + * PSRR at various frequencies +- Evaluate decoupling network design: + * Capacitor selection and placement + * Resonant frequency considerations + * ESR/ESL impacts +- Calculate voltage drops across power planes and traces +- Review protection mechanisms: + * Overvoltage protection + * Overcurrent protection + * Reverse polarity protection + * Inrush current limiting +- Perform power budget analysis including: + * Worst-case power consumption + * Thermal implications + * Efficiency calculations + * Power sequencing requirements -1. Hierarchical Design Review: -- Evaluate the overall hierarchical structure -- Assess modularity and organization -- Identify potential interface issues between hierarchical blocks +3. Detailed Signal Integrity Analysis: +- Analyze all critical signal paths: + * Rise/fall time calculations + * Propagation delay estimation + * Loading effects analysis + * Reflection analysis for high-speed signals +- Evaluate noise susceptibility: + * Common-mode noise rejection + * Power supply noise coupling + * Ground bounce effects + * Cross-talk between signals +- Review signal termination strategies +- Calculate signal margins and timing budgets +- Assess impedance matching requirements -2. Power Distribution Analysis: -- Review power path from input to regulated output -- Evaluate decoupling and filtering strategy -- Check for potential voltage drop issues -- Assess protection mechanisms +4. Extensive Component Analysis: +- Detailed review of each component: + * Electrical specifications + * Thermal characteristics + * Reliability metrics (MTBF) + * Cost considerations + * Availability and lifecycle status +- Footprint and package analysis: + * Thermal considerations + * Assembly requirements + * Reliability implications +- Component derating analysis: + * Voltage derating + * Current derating + * Temperature derating +- Alternative component recommendations -3. Signal Path Analysis: -- Trace critical signal paths -- Evaluate voltage divider networks -- Check for loading effects -- Identify potential noise coupling issues +5. Comprehensive Reliability and Safety Analysis: +- Detailed FMEA (Failure Mode and Effects Analysis): + * Component-level failure modes + * System-level failure impacts + * Criticality assessment + * Mitigation strategies +- Safety compliance review: + * Isolation requirements + * Clearance and creepage distances + * Protection against electrical hazards + * Thermal safety considerations +- Environmental considerations: + * Temperature range analysis + * Humidity effects + * Vibration resistance + * EMI/EMC compliance +- Reliability calculations and predictions -4. Component Selection Review: -- Assess component values and ratings -- Review footprint selections -- Check for standard vs specialized parts -- Evaluate thermal considerations +6. Manufacturing and Testing Considerations: +- DFM (Design for Manufacturing) analysis: + * Component placement optimization + * Assembly process requirements + * Test point accessibility + * Programming/debugging access +- Test strategy recommendations: + * Functional test coverage + * In-circuit test requirements + * Boundary scan capabilities + * Built-in self-test features +- Production cost optimization suggestions +- Quality control recommendations -5. Safety and Reliability: -- Identify potential failure modes -- Review protection mechanisms -- Assess thermal management -- Check compliance with basic safety practices +7. Detailed Technical Recommendations: +- Prioritized list of critical improvements needed +- Specific component value optimization calculations +- Alternative design approaches with trade-off analysis +- Performance enhancement suggestions with quantitative benefits +- Reliability improvement recommendations +- Cost reduction opportunities +- Maintenance and serviceability improvements -6. Specific Recommendations: -- List concrete suggestions for improvements -- Highlight any critical issues that need addressing -- Suggest alternative approaches where relevant -- Provide component value optimizations if needed +8. Compliance and Standards Review: +- Industry standard compliance analysis +- Regulatory requirements review +- Design guideline conformance +- Best practices comparison +- Documentation requirements +- Certification considerations -7. Design Best Practices: -- Compare against industry standard practices -- Identify any deviations from typical approaches -- Suggest documentation improvements -- Note any missing test points or debug features +For each section, provide: +- Detailed technical analysis +- Specific calculations where applicable +- Quantitative assessments +- Priority rankings for issues found +- Concrete improvement recommendations +- Cost-benefit analysis of suggested changes +- Risk assessment of identified issues -Please provide detailed technical reasoning for each point and specific recommendations where applicable. +Use industry standard terminology and provide specific technical details throughout the analysis. +Reference relevant standards, typical values, and best practices where applicable. +Support all recommendations with technical reasoning and specific benefits. """ return prompt - def analyze_circuit(self, circuit_description: str) -> Dict: - """Perform LLM analysis of the circuit.""" + def analyze_circuit( + self, + circuit_description: str, + output_file: Optional[str] = "circuit_llm_analysis.txt", + verbose: bool = True + ) -> Dict: + """ + Perform LLM analysis of the circuit. + + Args: + circuit_description: Description of the circuit to analyze + output_file: File to save the analysis results (None to skip saving) + verbose: Whether to print progress messages + + Returns: + Dictionary containing analysis results and metadata + """ + start_time = time.time() + + if verbose: + print("\n=== Starting Circuit Analysis with LLM ===") + print(f"Using provider: {self.provider.__class__.__name__}") + try: + # Generate and validate prompt + if verbose: + print("\nGenerating analysis prompt...") prompt = self._generate_analysis_prompt(circuit_description) + prompt_tokens = len(prompt.split()) + + if verbose: + print(f"Prompt generated ({prompt_tokens} estimated tokens)") + print("\nPrompt preview (first 200 chars):") + print(f"{prompt[:200]}...") + print("\nSending request to LLM...") + + # Configure for maximum response length + self.config['max_tokens'] = self.config.get('max_tokens', 4000) - response = self.client.messages.create( - model="claude-3-sonnet-20240229", - max_tokens=4000, - messages=[{ - "role": "user", - "content": prompt - }] + # Generate analysis + request_start = time.time() + analysis_result = self.provider.generate_analysis( + prompt, + model=self.model, + **self.config ) - return { - "success": True, - "analysis": response.content[0].text, - "timestamp": response.created + request_time = time.time() - request_start + + if not analysis_result["success"]: + raise Exception(analysis_result["error"]) + + # Add metadata to results + analysis_text = analysis_result["analysis"] + analysis_tokens = len(analysis_text.split()) + + results = { + **analysis_result, + "timestamp": int(datetime.now().timestamp()), + "request_time_seconds": request_time, + "prompt_tokens": prompt_tokens, + "response_tokens": analysis_tokens, + "total_time_seconds": time.time() - start_time } + if verbose: + print(f"\nResponse received in {request_time:.2f} seconds") + print(f"Response length: {len(analysis_text)} characters") + print(f"Estimated response tokens: {analysis_tokens}") + + # Save results if requested + if output_file: + if verbose: + print(f"\nSaving analysis to {output_file}...") + with open(output_file, "w") as f: + f.write(analysis_text) + if verbose: + print("Analysis saved successfully") + + if verbose: + print(f"\n=== Analysis completed in {results['total_time_seconds']:.2f} seconds ===") + + return results + except Exception as e: - return { + error_results = { "success": False, - "error": str(e) + "error": str(e), + "timestamp": int(datetime.now().timestamp()), + "total_time_seconds": time.time() - start_time } - - def save_analysis(self, analysis: Dict, output_file: str = "circuit_analysis.txt"): - """Save the analysis results to a file.""" - if analysis["success"]: - with open(output_file, "w") as f: - f.write(analysis["analysis"]) - else: - with open(output_file, "w") as f: - f.write(f"Analysis failed: {analysis['error']}") + + if verbose: + print(f"\nERROR: Analysis failed: {str(e)}") + + if output_file: + if verbose: + print(f"\nSaving error message to {output_file}...") + with open(output_file, "w") as f: + f.write(f"Analysis failed: {error_results['error']}") + if verbose: + print("Error message saved") + + if verbose: + print("\n=== Analysis failed ===") + + return error_results \ No newline at end of file diff --git a/src/skidl/llm_providers.py b/src/skidl/llm_providers.py new file mode 100644 index 000000000..adc45ca7c --- /dev/null +++ b/src/skidl/llm_providers.py @@ -0,0 +1,164 @@ +"""Module for LLM provider implementations for circuit analysis.""" + +from abc import ABC, abstractmethod +import os +from typing import Dict, Optional, Any +import anthropic +from openai import OpenAI +from anthropic import Anthropic +import requests + +class LLMProvider(ABC): + """Abstract base class for LLM providers.""" + + def __init__(self, api_key: Optional[str] = None): + self.api_key = api_key + self.client = self._initialize_client() + + @abstractmethod + def _initialize_client(self) -> Any: + """Initialize the API client.""" + pass + + @abstractmethod + def generate_analysis(self, prompt: str, **kwargs) -> Dict: + """Generate analysis using the LLM.""" + pass + +class AnthropicProvider(LLMProvider): + """Provider for Anthropic's Claude models.""" + + def _initialize_client(self) -> Anthropic: + api_key = self.api_key or os.getenv('ANTHROPIC_API_KEY') + if not api_key: + raise ValueError("Anthropic API key not provided") + return Anthropic(api_key=api_key) + + def generate_analysis(self, prompt: str, **kwargs) -> Dict: + try: + # Use the specified model or default to claude-3-sonnet + model = kwargs.get('model') or "claude-3-sonnet-20240229" + max_tokens = kwargs.get('max_tokens', 8000) + + response = self.client.messages.create( + model=model, + max_tokens=max_tokens, + messages=[{ + "role": "user", + "content": prompt + }] + ) + + # Use current timestamp since Anthropic response doesn't include it + from datetime import datetime + return { + "success": True, + "analysis": response.content[0].text, + "timestamp": int(datetime.now().timestamp()), + "provider": "anthropic" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "provider": "anthropic" + } + +class OpenAIProvider(LLMProvider): + """Provider for OpenAI's GPT models.""" + + def _initialize_client(self) -> OpenAI: + api_key = self.api_key or os.getenv('OPENAI_API_KEY') + if not api_key: + raise ValueError("OpenAI API key not provided") + return OpenAI(api_key=api_key) + + def generate_analysis(self, prompt: str, **kwargs) -> Dict: + try: + model = kwargs.get('model', 'gpt-4-turbo-preview') + max_tokens = kwargs.get('max_tokens', 4000) + + response = self.client.chat.completions.create( + model=model, + messages=[{ + "role": "user", + "content": prompt + }], + max_tokens=max_tokens + ) + + return { + "success": True, + "analysis": response.choices[0].message.content, + "timestamp": response.created, + "provider": "openai" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "provider": "openai" + } + +class OpenRouterProvider(LLMProvider): + """Provider for OpenRouter API access to various LLMs.""" + + def _initialize_client(self) -> None: + self.api_key = self.api_key or os.getenv('OPENROUTER_API_KEY') + if not self.api_key: + raise ValueError("OpenRouter API key not provided") + # OpenRouter uses direct HTTP requests, so no client initialization needed + return None + + def generate_analysis(self, prompt: str, **kwargs) -> Dict: + try: + model = kwargs.get('model', 'anthropic/claude-3-opus-20240229') + max_tokens = kwargs.get('max_tokens', 4000) + + headers = { + "Authorization": f"Bearer {self.api_key}", + "HTTP-Referer": kwargs.get('referer', 'https://skidl.org'), # Replace with your domain + "X-Title": kwargs.get('title', 'SKiDL Circuit Analysis') + } + + response = requests.post( + "https://openrouter.ai/api/v1/chat/completions", + headers=headers, + json={ + "model": model, + "messages": [{ + "role": "user", + "content": prompt + }], + "max_tokens": max_tokens + } + ) + + response.raise_for_status() + result = response.json() + + return { + "success": True, + "analysis": result['choices'][0]['message']['content'], + "timestamp": result['created'], + "provider": "openrouter" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "provider": "openrouter" + } + +def get_provider(provider_name: str, api_key: Optional[str] = None) -> LLMProvider: + """Factory function to get the appropriate LLM provider.""" + providers = { + "anthropic": AnthropicProvider, + "openai": OpenAIProvider, + "openrouter": OpenRouterProvider + } + + if provider_name not in providers: + raise ValueError(f"Unknown provider: {provider_name}") + + return providers[provider_name](api_key=api_key) \ No newline at end of file From c58e260998a304ffe8fc4164d2021c2750da7065 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 17:55:07 -0800 Subject: [PATCH 06/73] add to circuit script to use new logic --- setup.py | 1 + skidl_test.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 94d485203..674979faf 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ #'PySpice; python_version >= "3.0"', "graphviz", "deprecation", + "anthropic >= 0.18.0", ] test_requirements = [ diff --git a/skidl_test.py b/skidl_test.py index fc7be8995..2d3b7a155 100644 --- a/skidl_test.py +++ b/skidl_test.py @@ -1,4 +1,5 @@ from skidl import * +import os @subcircuit def rc_filter(inp, outp, gnd): @@ -90,4 +91,30 @@ def complete_circuit(): # Get circuit info circuit_description = get_circuit_info() -analyze_with_llm() \ No newline at end of file + + +# Using Anthropic Claude (original behavior) +analyzer = SkidlCircuitAnalyzer( + provider="anthropic", + api_key=os.getenv("ANTHROPIC_API_KEY"), + model="claude-3-sonnet-20240229" +) + +# # Using OpenAI +# analyzer = SkidlCircuitAnalyzer( +# provider="openai", +# api_key="your_openai_key", +# model="gpt-4-turbo-preview" # optional +# ) + +# # Using OpenRouter +# analyzer = SkidlCircuitAnalyzer( +# provider="openrouter", +# api_key="your_openrouter_key", +# model="anthropic/claude-3-opus-20240229", # optional +# referer="your_domain", # required for OpenRouter +# title="Your App Name" # optional +# ) + +# Analyze circuit with any provider +results = analyzer.analyze_circuit(circuit_description) From 77f87c21cf8c7a21149d31a1d1acd94c56a74158 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 18:38:55 -0800 Subject: [PATCH 07/73] artifacts from changes --- circuit_description.txt | 286 +++++++++++++++++++++++++++++++++++++++ circuit_llm_analysis.txt | 223 ++++++++++++++++++++++++++++++ 2 files changed, 509 insertions(+) create mode 100644 circuit_description.txt create mode 100644 circuit_llm_analysis.txt diff --git a/circuit_description.txt b/circuit_description.txt new file mode 100644 index 000000000..d6432472a --- /dev/null +++ b/circuit_description.txt @@ -0,0 +1,286 @@ +Circuit Description: +======================================== +Circuit Name: +Top Level Hierarchy: top + +Hierarchy Details: +---------------------------------------- +Hierarchy Level: top.complete_circuit0.double_divider0.output_termination0 +Parts: + Part: R6 + Name: R + Value: 100K + Footprint: Resistor_SMD.pretty:R_0805_2012Metric + Pins: + 1/~: N$4 + 2/~: N$5 + Nets: + Net: N$4 + Local Connections: + R6.1/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.voltage_divider1: + R4.2/~ + R5.1/~ + Net: N$5 + Local Connections: + R6.2/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.voltage_divider0: + R3.2/~ + top.complete_circuit0.double_divider0.voltage_divider1: + R5.2/~ + top.complete_circuit0.power_section0.input_protection0: + C1.2/~ + top.complete_circuit0.power_section0.rc_filter0: + C4.2/~ + top.complete_circuit0.power_section0.voltage_regulator0: + C2.2/~ + C3.2/~ + U1.2/GND +_____________________________________________________ +Hierarchy Level: top.complete_circuit0.double_divider0.voltage_divider0 +Parts: + Part: R2 + Name: R + Value: 1K + Footprint: Resistor_SMD.pretty:R_0805_2012Metric + Pins: + 1/~: N$3 + 2/~: N$10 + Part: R3 + Name: R + Value: 500 + Footprint: Resistor_SMD.pretty:R_0805_2012Metric + Pins: + 1/~: N$10 + 2/~: N$5 + Nets: + Net: N$3 + Local Connections: + R2.1/~ + Connected to Other Hierarchies: + top.complete_circuit0.power_section0.rc_filter0: + C4.1/~ + R1.2/~ + Net: N$5 + Local Connections: + R3.2/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.output_termination0: + R6.2/~ + top.complete_circuit0.double_divider0.voltage_divider1: + R5.2/~ + top.complete_circuit0.power_section0.input_protection0: + C1.2/~ + top.complete_circuit0.power_section0.rc_filter0: + C4.2/~ + top.complete_circuit0.power_section0.voltage_regulator0: + C2.2/~ + C3.2/~ + U1.2/GND + Net: N$9 + Local Connections: + R2.2/~ + R3.1/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.voltage_divider1: + R4.1/~ +_____________________________________________________ +Hierarchy Level: top.complete_circuit0.double_divider0.voltage_divider1 +Parts: + Part: R4 + Name: R + Value: 1K + Footprint: Resistor_SMD.pretty:R_0805_2012Metric + Pins: + 1/~: N$9 + 2/~: N$11 + Part: R5 + Name: R + Value: 500 + Footprint: Resistor_SMD.pretty:R_0805_2012Metric + Pins: + 1/~: N$11 + 2/~: N$5 + Nets: + Net: N$4 + Local Connections: + R4.2/~ + R5.1/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.output_termination0: + R6.1/~ + Net: N$5 + Local Connections: + R5.2/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.output_termination0: + R6.2/~ + top.complete_circuit0.double_divider0.voltage_divider0: + R3.2/~ + top.complete_circuit0.power_section0.input_protection0: + C1.2/~ + top.complete_circuit0.power_section0.rc_filter0: + C4.2/~ + top.complete_circuit0.power_section0.voltage_regulator0: + C2.2/~ + C3.2/~ + U1.2/GND + Net: N$9 + Local Connections: + R4.1/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.voltage_divider0: + R2.2/~ + R3.1/~ +_____________________________________________________ +Hierarchy Level: top.complete_circuit0.power_section0.input_protection0 +Parts: + Part: C1 + Name: C + Value: 100uF + Footprint: Capacitor_THT:CP_Radial_D10.0mm_P5.00mm + Pins: + 1/~: N$1 + 2/~: N$5 + Part: D1 + Name: D + Value: 1N4148W + Footprint: Diode_SMD:D_SOD-123 + Pins: + 1/K: N$6 + 2/A: N$1 + Nets: + Net: N$1 + Local Connections: + C1.1/~ + D1.2/A + Net: N$5 + Local Connections: + C1.2/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.output_termination0: + R6.2/~ + top.complete_circuit0.double_divider0.voltage_divider0: + R3.2/~ + top.complete_circuit0.double_divider0.voltage_divider1: + R5.2/~ + top.complete_circuit0.power_section0.rc_filter0: + C4.2/~ + top.complete_circuit0.power_section0.voltage_regulator0: + C2.2/~ + C3.2/~ + U1.2/GND + Net: N$6 + Local Connections: + D1.1/K + Connected to Other Hierarchies: + top.complete_circuit0.power_section0.voltage_regulator0: + C2.1/~ + U1.1/VI +_____________________________________________________ +Hierarchy Level: top.complete_circuit0.power_section0.rc_filter0 +Parts: + Part: C4 + Name: C + Value: 0.1uF + Footprint: Capacitor_SMD:C_0805_2012Metric + Pins: + 1/~: N$8 + 2/~: N$5 + Part: R1 + Name: R + Value: 10K + Footprint: Resistor_SMD.pretty:R_0805_2012Metric + Pins: + 1/~: N$2 + 2/~: N$8 + Nets: + Net: N$2 + Local Connections: + R1.1/~ + Connected to Other Hierarchies: + top.complete_circuit0.power_section0.voltage_regulator0: + C3.1/~ + U1.3/VO + Net: N$3 + Local Connections: + C4.1/~ + R1.2/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.voltage_divider0: + R2.1/~ + Net: N$5 + Local Connections: + C4.2/~ + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.output_termination0: + R6.2/~ + top.complete_circuit0.double_divider0.voltage_divider0: + R3.2/~ + top.complete_circuit0.double_divider0.voltage_divider1: + R5.2/~ + top.complete_circuit0.power_section0.input_protection0: + C1.2/~ + top.complete_circuit0.power_section0.voltage_regulator0: + C2.2/~ + C3.2/~ + U1.2/GND +_____________________________________________________ +Hierarchy Level: top.complete_circuit0.power_section0.voltage_regulator0 +Parts: + Part: C2 + Name: C + Value: 10uF + Footprint: Capacitor_SMD:C_0805_2012Metric + Pins: + 1/~: N$6 + 2/~: N$5 + Part: C3 + Name: C + Value: 10uF + Footprint: Capacitor_SMD:C_0805_2012Metric + Pins: + 1/~: N$7 + 2/~: N$5 + Part: U1 + Name: LM7805_TO220 + Value: LM7805_TO220 + Footprint: Package_TO_SOT_THT:TO-220-3_Vertical + Pins: + 1/VI: N$6 + 2/GND: N$5 + 3/VO: N$7 + Nets: + Net: N$2 + Local Connections: + C3.1/~ + U1.3/VO + Connected to Other Hierarchies: + top.complete_circuit0.power_section0.rc_filter0: + R1.1/~ + Net: N$5 + Local Connections: + C2.2/~ + C3.2/~ + U1.2/GND + Connected to Other Hierarchies: + top.complete_circuit0.double_divider0.output_termination0: + R6.2/~ + top.complete_circuit0.double_divider0.voltage_divider0: + R3.2/~ + top.complete_circuit0.double_divider0.voltage_divider1: + R5.2/~ + top.complete_circuit0.power_section0.input_protection0: + C1.2/~ + top.complete_circuit0.power_section0.rc_filter0: + C4.2/~ + Net: N$6 + Local Connections: + C2.1/~ + U1.1/VI + Connected to Other Hierarchies: + top.complete_circuit0.power_section0.input_protection0: + D1.1/K +=============== END CIRCUIT =============== \ No newline at end of file diff --git a/circuit_llm_analysis.txt b/circuit_llm_analysis.txt new file mode 100644 index 000000000..43b593788 --- /dev/null +++ b/circuit_llm_analysis.txt @@ -0,0 +1,223 @@ +As an expert electronics engineer, I will provide a comprehensive analysis of the given electronic circuit design, covering various aspects such as design architecture, power distribution, signal integrity, component analysis, reliability and safety, manufacturing and testing considerations, technical recommendations, and compliance review. Please note that this analysis will be extensive and detailed. + +1. Comprehensive Design Architecture Review: + - Overall Hierarchical Structure and Design Patterns: + The circuit design follows a hierarchical structure with three main blocks: power_section0, double_divider0, and output_termination0. This modular approach promotes reusability and maintainability by separating concerns. However, the naming conventions could be improved for better readability and understanding. + + - Modularity, Reusability, and Maintainability: + The power_section0 block encapsulates the input protection, RC filtering, and voltage regulation functions, promoting reusability. The double_divider0 block contains two voltage dividers, which could be refactored into a single reusable module for better maintainability. + + - Interface Definitions and Protocols: + The circuit design does not explicitly define interfaces or protocols between blocks, which could lead to integration issues and potential signal integrity problems. Clearly defined interfaces and protocols would improve modularity and testability. + + - Signal Flow and Control Paths: + The signal flow is straightforward, starting from the input protection, followed by voltage regulation, and then feeding into the voltage dividers. However, there is no apparent control logic or feedback mechanism, which might limit the circuit's adaptability and responsiveness. + + - Design Scalability and Expansion Capabilities: + The modular design allows for potential scalability and expansion by adding or modifying blocks. However, the lack of well-defined interfaces and protocols could hinder seamless integration of new modules or functionalities. + + - Potential Bottlenecks: + No major architectural bottlenecks are apparent from the provided design. However, a thorough analysis of signal paths, component specifications, and power distribution is necessary to identify potential performance limitations. + +2. In-depth Power Distribution Analysis: + - Power Distribution Network: + The power distribution network starts from the input protection (D1, C1), followed by the voltage regulator (U1, C2, C3), and then branches out to the voltage dividers (R2, R3, R4, R5) and the output termination (R6). The RC filter (R1, C4) is placed after the voltage regulator. + + - Voltage Regulation Performance: + - Load Regulation Calculations: + To calculate the load regulation, we need to know the input voltage range, output voltage, and the regulator's load regulation specification. Assuming a typical input voltage range of 9-12V and an output voltage of 5V, the load regulation can be estimated using the LM7805 datasheet. + + Load Regulation = (ΔVout / Vout) * 100% + For the LM7805, the typical load regulation is 5 mV (maximum 100 mV) for a load current change of 5 mA to 1.5A. + Load Regulation = (5 mV / 5V) * 100% = 0.1% (typical) + + - Line Regulation Performance: + Line regulation is the ability of the regulator to maintain a constant output voltage despite changes in input voltage. The LM7805 datasheet specifies a typical line regulation of 10 mV (maximum 30 mV) for an input voltage change of 7V to 25V. + + Line Regulation = (ΔVout / Vout) * 100% + Line Regulation = (10 mV / 5V) * 100% = 0.2% (typical) + + - Transient Response Characteristics: + The transient response characteristics, such as rise time, overshoot, and settling time, depend on the output capacitance (C3) and the regulator's transient response specifications. These characteristics should be evaluated based on the load transient requirements and regulator datasheet. + + - PSRR (Power Supply Rejection Ratio): + The PSRR quantifies the regulator's ability to reject input noise and ripple. The LM7805 datasheet specifies a typical PSRR of 65 dB at 1 kHz, which should be evaluated based on the expected input noise frequencies. + + - Decoupling Network Design: + - Capacitor Selection and Placement: + The input capacitor (C1) is a 100 μF electrolytic capacitor, which is suitable for bulk decoupling. The output capacitors (C2, C3) are 10 μF ceramic capacitors, which provide high-frequency decoupling. However, additional high-frequency decoupling capacitors may be needed closer to the voltage dividers and other loads. + + - Resonant Frequency Considerations: + The resonant frequency of the decoupling network should be evaluated to ensure it does not coincide with any critical frequencies in the system. This analysis requires knowledge of the parasitic inductances and capacitances. + + - ESR/ESL Impacts: + The equivalent series resistance (ESR) and equivalent series inductance (ESL) of the decoupling capacitors can impact the effectiveness of the decoupling network, especially at high frequencies. Selecting capacitors with low ESR and ESL values is recommended. + + - Voltage Drops across Power Planes and Traces: + To accurately calculate the voltage drops across power planes and traces, detailed information about trace lengths, widths, and current densities is required. However, it is essential to ensure that the voltage drops are within acceptable limits to prevent undershoots or overshoots at the loads. + + - Protection Mechanisms: + - Overvoltage Protection: + The circuit does not appear to have explicit overvoltage protection. Adding a Zener diode or transient voltage suppressor (TVS) diode at the input could provide overvoltage protection. + + - Overcurrent Protection: + The circuit does not have any overcurrent protection mechanism. Adding a fuse or resettable polyfuse in series with the input could provide overcurrent protection. + + - Reverse Polarity Protection: + The input protection diode (D1) provides reverse polarity protection by preventing current flow in the reverse direction. + + - Inrush Current Limiting: + The circuit does not have any inrush current limiting mechanism. Adding an NTC (Negative Temperature Coefficient) thermistor or a soft-start circuit could limit the inrush current during power-up. + + - Power Budget Analysis: + - Worst-case Power Consumption: + To calculate the worst-case power consumption, the maximum current draw of all loads must be known. This information is not provided in the circuit description. + + - Thermal Implications: + The thermal implications depend on the power dissipation of the components, especially the voltage regulator (U1) and the power resistors (R2, R3, R4, R5, R6). Proper heat sinking and thermal management should be considered. + + - Efficiency Calculations: + The efficiency of the voltage regulator can be estimated from the datasheet. However, the overall system efficiency would also depend on the power dissipation in the resistors and other components. + + - Power Sequencing Requirements: + The circuit does not appear to have any specific power sequencing requirements. However, if multiple voltage domains are present, proper power sequencing may be necessary to avoid potential issues. + +3. Detailed Signal Integrity Analysis: + - Critical Signal Paths: + - Rise/Fall Time Calculations: + To accurately calculate the rise and fall times of critical signals, detailed information about the driving and loading conditions is required. This includes the output impedance of the drivers, input capacitances of the loads, and trace lengths. + + - Propagation Delay Estimation: + Propagation delays depend on the trace lengths, propagation velocity in the PCB material, and the loading conditions. Detailed PCB layout information and component specifications are needed for accurate propagation delay estimation. + + - Loading Effects Analysis: + The loading effects on critical signals should be analyzed to ensure that the driving circuits can adequately drive the loads without excessive signal degradation. This includes the input capacitances of the loads and the impedance matching between the source and load. + + - Reflection Analysis for High-speed Signals: + If the circuit involves high-speed signals (e.g., above a few MHz), a reflection analysis should be performed to ensure that signal reflections do not cause excessive ringing or overshoot. This analysis requires knowledge of the trace impedances and termination strategies. + + - Noise Susceptibility: + - Common-mode Noise Rejection: + The circuit does not appear to have any differential signaling or common-mode noise rejection provisions. If common-mode noise is a concern, appropriate shielding, filtering, or differential signaling techniques may be required. + + - Power Supply Noise Coupling: + The decoupling capacitors (C1, C2, C3, C4) help mitigate power supply noise coupling. However, additional decoupling or filtering may be needed if high-frequency noise is present on the power rails. + + - Ground Bounce Effects: + Ground bounce effects can occur due to high current transients and shared ground impedances. Proper layout techniques, such as minimizing ground loops and using star ground topologies, can help mitigate ground bounce. + + - Cross-talk between Signals: + Cross-talk between signals can be minimized by maintaining proper spacing between traces, using ground planes as shields, and employing differential signaling techniques if necessary. + + - Signal Termination Strategies: + The output termination block (output_termination0) consists of a single 100 kΩ resistor (R6). This resistor may not provide adequate termination for high-speed signals or impedance matching, depending on the signal characteristics and trace impedances. + + - Signal Margins and Timing Budgets: + Without detailed information about the signal levels, timing requirements, and loading conditions, it is difficult to accurately calculate signal margins and timing budgets. + + - Impedance Matching Requirements: + Impedance matching is crucial for high-speed signals to minimize reflections and ensure proper signal integrity. The circuit design does not provide information about the trace impedances or termination strategies, making it challenging to evaluate impedance matching requirements. + +4. Extensive Component Analysis: + - Detailed Component Review: + - Electrical Specifications: + The circuit incorporates passive components (resistors and capacitors) and an LM7805 voltage regulator. The electrical specifications of these components should be reviewed in detail, including their voltage and current ratings, temperature coefficients, and tolerances. + + - Thermal Characteristics: + The thermal characteristics of the components, particularly the voltage regulator (U1) and power resistors (R2, R3, R4, R5, R6), should be evaluated. This includes analyzing their power dissipation, thermal resistance, and maximum operating temperatures. + + - Reliability Metrics (MTBF): + The mean time between failures (MTBF) of the components should be reviewed to estimate the overall reliability of the circuit. This information is typically available in the component datasheets or from the manufacturers. + + - Cost Considerations: + The cost of the components should be evaluated to optimize the overall bill of materials (BOM) cost while maintaining the required performance and reliability. + + - Availability and Lifecycle Status: + The availability and lifecycle status of the components should be considered to ensure a stable and long-term supply chain. End-of-life (EOL) or obsolescence issues should be addressed, if applicable. + + - Footprint and Package Analysis: + - Thermal Considerations: + The footprints and packages of the components, especially the voltage regulator (U1) and power resistors, should be evaluated for their thermal dissipation capabilities. Proper heat sinking or airflow may be required for components with high power dissipation. + + - Assembly Requirements: + The footprints and packages should be evaluated for their assembly requirements, such as solderability, lead-free compliance, and compatibility with the manufacturing processes. + + - Reliability Implications: + The package types and footprints can have implications on the reliability of the components. For example, surface-mount components generally have better vibration resistance compared to through-hole components. + + - Component Derating Analysis: + - Voltage Derating: + The applied voltages across the components should be derated to ensure a sufficient safety margin and longer component lifetimes. Typical voltage derating factors should be applied based on industry standards and component specifications. + + - Current Derating: + The current flowing through the components should be derated to prevent excessive power dissipation and ensure reliable operation. Current derating factors should be applied based on the component specifications and operating conditions. + + - Temperature Derating: + The operating temperatures of the components should be derated to account for temperature variations, self-heating effects, and to extend component lifetimes. Temperature derating factors should be applied based on the component specifications and thermal characteristics. + + - Alternative Component Recommendations: + Based on the detailed component analysis, alternative components may be recommended to improve performance, reliability, cost-effectiveness, or availability. These recommendations should consider factors such as electrical specifications, thermal characteristics, footprints, and lifecycle status. + +5. Comprehensive Reliability and Safety Analysis: + - Detailed FMEA (Failure Mode and Effects Analysis): + - Component-level Failure Modes: + Potential failure modes of each component should be identified and analyzed. This includes open-circuit failures, short-circuit failures, parametric drifts, and other failure mechanisms specific to the component types. + + - System-level Failure Impacts: + The impact of each component failure mode on the overall system functionality should be evaluated. This includes analyzing the propagation of failures and their effects on other components or subsystems. + + - Criticality Assessment: + The criticality of each failure mode should be assessed based on factors such as severity, probability of occurrence, and detectability. This assessment helps prioritize mitigation strategies and allocate resources effectively. + + - Mitigation Strategies: + Mitigation strategies should be developed for critical failure modes. These may include component redundancy, fault-tolerant design techniques, protection circuits, or diagnostic and monitoring capabilities. + + - Safety Compliance Review: + - Isolation Requirements: + If the circuit operates with high voltages or interfaces with external systems, appropriate isolation requirements should be evaluated to ensure user safety and compliance with relevant standards. + + - Clearance and Creepage Distances: + The clearance and creepage distances between conductive parts should be reviewed to prevent electrical arcing or tracking, especially in high-voltage or high-pollution environments. + + - Protection against Electrical Hazards: + Appropriate protection mechanisms, such as fusing, current limiting, and insulation barriers, should be implemented to mitigate electrical hazards and ensure user safety. + + - Thermal Safety Considerations: + The thermal safety aspects of the circuit should be evaluated, including component operating temperatures, touch temperatures, and potential fire hazards. Proper thermal management and insulation techniques may be required. + + - Environmental Considerations: + - Temperature Range Analysis: + The circuit should be designed to operate reliably within the specified temperature range. Derating factors and appropriate component selections should be applied based on the temperature range requirements. + + - Humidity Effects: + The effects of humidity on the circuit components and interconnections should be evaluated. Conformal coatings, moisture-resistant materials, or hermetic sealing may be necessary in high-humidity environments. + + - Vibration Resistance: + If the circuit is subjected to vibrations or mechanical shocks, the component footprints, interconnections, and mechanical mounting should be designed to withstand these environmental conditions. + + - EMI/EMC Compliance: + The circuit should be evaluated for electromagnetic interference (EMI) and electromagnetic compatibility (EMC) compliance. Appropriate shielding, filtering, and layout techniques may be required to mitigate EMI/EMC issues. + + - Reliability Calculations and Predictions: + Based on the component reliability data and failure mode analysis, reliability calculations and predictions should be performed for the overall circuit. This may involve techniques such as Reliability Block Diagrams (RBDs), Fault Tree Analysis (FTA), or Monte Carlo simulations. + +6. Manufacturing and Testing Considerations: + - DFM (Design for Manufacturing) Analysis: + - Component Placement Optimization: + The component placement on the PCB should be optimized for manufacturing considerations, such as pick-and-place machine accessibility, thermal management, and signal integrity. + + - Assembly Process Requirements: + The circuit design should be reviewed for compatibility with the intended assembly processes, such as wave soldering, reflow soldering, or hand assembly. Component footprints, clearances, and thermal profiles should be considered. + + - Test Point Accessibility: + Appropriate test points should be included in the design to facilitate testing and debugging during manufacturing and field service. The accessibility of these test points should be evaluated. + + - Programming/Debugging Access: + If the circuit includes programmable devices or microcontrollers, provisions for programming and debugging access should be made, such as dedicated programming headers or in-circuit programming interfaces. + + - Test Strategy Recommendations: + - Functional Test Coverage: + A comprehensive functional test strategy should be developed to ensure thorough testing of the circuit's functionality, including edge cases and corner conditions. + + - In-circuit Test Requirements: + In-circuit testing requirements should be evaluated, such \ No newline at end of file From 6eb501c6c385862279912d568be20ebaee24e565 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 18:39:12 -0800 Subject: [PATCH 08/73] modify chat completion script --- llm_chat_completion_script.py | 178 ++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 llm_chat_completion_script.py diff --git a/llm_chat_completion_script.py b/llm_chat_completion_script.py new file mode 100644 index 000000000..4b304f0db --- /dev/null +++ b/llm_chat_completion_script.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +""" +This script is used to collect all the code files needed for the LLM chat completion task. +It will search for specific files in a given directory and combine their contents into a single output file. + +Edit the "INTRO_MESSAGE" variable to add a message at the start of the output file. + +This tool is useful for preparing code snippets for AI-based code completion tasks. +""" + +#============================================================================== +# QUICK EDIT CONFIGURATION - Modify these values as needed +#============================================================================== + +# Where to look for files +ROOT_DIRECTORY = "/Users/shanemattner/Desktop/skidl" + +# Where to save the combined output +OUTPUT_FILE = "collected_code.txt" + +# What files to collect - specify full relative paths from ROOT_DIRECTORY +TARGET_FILES = [ + 'src/skidl/circuit.py', + 'src/skidl/llm_providers.py', + 'src/skidl/circuit_analyzer.py', + 'skidl_test.py', +] + +# Message to add at the start of the output file +INTRO_MESSAGE = """ +Help me develop code for implementing the LLM analysis of the circuit. +""" + +#============================================================================== +# Script Implementation - No need to modify below this line +#============================================================================== + +import os +from typing import List +from dataclasses import dataclass + +@dataclass +class FileCollectorConfig: + """Configuration class to store all script parameters""" + root_directory: str + output_filename: str + target_files: List[str] + intro_message: str + +def create_config_from_settings() -> FileCollectorConfig: + """Creates configuration object from the settings defined at the top of the script""" + return FileCollectorConfig( + root_directory=ROOT_DIRECTORY, + output_filename=OUTPUT_FILE, + target_files=TARGET_FILES, + intro_message=INTRO_MESSAGE + ) + +def normalize_path(path: str) -> str: + """Normalize a path by replacing backslashes with forward slashes""" + return path.replace('\\', '/') + +def is_target_file(filepath: str, root_dir: str, target_files: List[str]) -> bool: + """ + Check if a filepath matches our target criteria. + + Args: + filepath: Full path of the file to check + root_dir: Root directory for relative path calculation + target_files: List of target relative paths + """ + # Convert the full path to a relative path from root_dir + try: + rel_path = os.path.relpath(filepath, root_dir) + rel_path = normalize_path(rel_path) + + # Debug print + print(f"Checking file: {rel_path}") + + # Check if this relative path matches any target path + return rel_path in target_files + except ValueError: + # This can happen if filepath is on a different drive than root_dir + return False + +def find_target_files(config: FileCollectorConfig) -> List[str]: + """ + Search for target files in the root directory. + + Args: + config: Configuration object containing search parameters + + Returns: + List[str]: List of full file paths for matching files + """ + collected_files = [] + + print(f"\nSearching in root directory: {config.root_directory}") + print(f"Looking for files with these relative paths:") + for target in config.target_files: + print(f"- {target}") + print() + + # Walk through the directory tree + for dirpath, dirnames, filenames in os.walk(config.root_directory): + print(f"\nExamining directory: {dirpath}") + + for filename in filenames: + full_path = os.path.join(dirpath, filename) + if os.path.isfile(full_path) and is_target_file(full_path, config.root_directory, config.target_files): + collected_files.append(full_path) + print(f"Added to collection: {full_path}") + + return sorted(collected_files) + +def write_combined_file(collected_files: List[str], config: FileCollectorConfig) -> None: + """ + Write all collected file contents to a single output file. + + Args: + collected_files: List of file paths to combine + config: Configuration object containing output settings + """ + with open(config.output_filename, 'w') as out_file: + # Write the introduction message + out_file.write(config.intro_message + "\n") + + # Process each collected file + total_lines = 0 + for file_path in collected_files: + try: + # Read and write each file's contents with clear separation + with open(file_path, 'r') as input_file: + content = input_file.read() + relative_path = os.path.relpath(file_path, config.root_directory) + relative_path = normalize_path(relative_path) + + # Add clear separators around file content + out_file.write(f"\n/* Begin of file: {relative_path} */\n") + out_file.write(content) + out_file.write(f"\n/* End of file: {relative_path} */\n") + + # Print statistics for monitoring + num_lines = len(content.splitlines()) + total_lines += num_lines + print(f"{relative_path}: {num_lines} lines") + + except Exception as e: + print(f"Error processing {file_path}: {e}") + print(f"Total lines written: {total_lines}") + +def main(): + """Main execution function""" + # Create configuration from settings + config = create_config_from_settings() + + # Find all matching files + collected_files = find_target_files(config) + + if not collected_files: + print("\nNo matching files found! Check your ROOT_DIRECTORY and TARGET_FILES settings.") + return + + print("\nFound files:") + for f in collected_files: + print(f"- {f}") + + # Combine files into output + print("\nWriting combined output file...") + write_combined_file(collected_files, config) + + # Print summary + print(f"\nProcessed {len(collected_files)} files") + print(f"Output saved to: {config.output_filename}") + +if __name__ == "__main__": + main() \ No newline at end of file From 9a6b4ff8782a6a0c8eea289b0faa05b861cbe935 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 18:56:44 -0800 Subject: [PATCH 09/73] Refactor prompt logic --- circuit_llm_analysis.txt | 267 ++++++++--------------------- skidl_test.py => skidl_llm_test.py | 9 +- src/skidl/circuit_analyzer.py | 262 ++++++++++++++++++---------- src/skidl/llm_providers.py | 5 +- 4 files changed, 259 insertions(+), 284 deletions(-) rename skidl_test.py => skidl_llm_test.py (92%) diff --git a/circuit_llm_analysis.txt b/circuit_llm_analysis.txt index 43b593788..a8f9dc24c 100644 --- a/circuit_llm_analysis.txt +++ b/circuit_llm_analysis.txt @@ -1,223 +1,108 @@ -As an expert electronics engineer, I will provide a comprehensive analysis of the given electronic circuit design, covering various aspects such as design architecture, power distribution, signal integrity, component analysis, reliability and safety, manufacturing and testing considerations, technical recommendations, and compliance review. Please note that this analysis will be extensive and detailed. +As an expert electronics engineer, I'm happy to provide a comprehensive professional analysis of the given circuit design. Please note that this analysis will be based solely on the information provided in the circuit description, and any additional requirements or constraints should be clearly specified. -1. Comprehensive Design Architecture Review: - - Overall Hierarchical Structure and Design Patterns: - The circuit design follows a hierarchical structure with three main blocks: power_section0, double_divider0, and output_termination0. This modular approach promotes reusability and maintainability by separating concerns. However, the naming conventions could be improved for better readability and understanding. +1. Overall Design Architecture Review: - - Modularity, Reusability, and Maintainability: - The power_section0 block encapsulates the input protection, RC filtering, and voltage regulation functions, promoting reusability. The double_divider0 block contains two voltage dividers, which could be refactored into a single reusable module for better maintainability. +- The hierarchical structure of the circuit is well-organized, with subsystems logically grouped into modules (e.g., double_divider0, power_section0). +- The modular approach promotes reusability and maintainability, as individual sections can be replaced or updated without affecting the entire system. +- The signal flow and control paths are well-defined, with clear connectivity between modules and components. +- The design appears to be scalable, as additional components or subsystems can be integrated into the existing hierarchy. - - Interface Definitions and Protocols: - The circuit design does not explicitly define interfaces or protocols between blocks, which could lead to integration issues and potential signal integrity problems. Clearly defined interfaces and protocols would improve modularity and testability. +MISSING INFORMATION CHECK: +- No undefined pin functions or missing part values were identified in the provided information. +- All necessary subsystems for the intended functionality (voltage regulation, voltage division, input protection, and filtering) appear to be present. - - Signal Flow and Control Paths: - The signal flow is straightforward, starting from the input protection, followed by voltage regulation, and then feeding into the voltage dividers. However, there is no apparent control logic or feedback mechanism, which might limit the circuit's adaptability and responsiveness. +2. Power Supply Design Review: - - Design Scalability and Expansion Capabilities: - The modular design allows for potential scalability and expansion by adding or modifying blocks. However, the lack of well-defined interfaces and protocols could hinder seamless integration of new modules or functionalities. +- The input protection section (input_protection0) includes a diode (D1) for reverse polarity protection and a bulk capacitor (C1) for filtering high-frequency noise. +- The voltage regulator section (voltage_regulator0) uses an LM7805 linear regulator (U1) with appropriate input and output capacitors (C2, C3) for stability and ripple reduction. +- The RC filter section (rc_filter0) provides additional high-frequency noise filtering on the regulated output using a resistor (R1) and capacitor (C4). - - Potential Bottlenecks: - No major architectural bottlenecks are apparent from the provided design. However, a thorough analysis of signal paths, component specifications, and power distribution is necessary to identify potential performance limitations. +MISSING INFORMATION CHECK: +- No missing information regarding the power supply design was identified. -2. In-depth Power Distribution Analysis: - - Power Distribution Network: - The power distribution network starts from the input protection (D1, C1), followed by the voltage regulator (U1, C2, C3), and then branches out to the voltage dividers (R2, R3, R4, R5) and the output termination (R6). The RC filter (R1, C4) is placed after the voltage regulator. +3. Signal Integrity Analysis: - - Voltage Regulation Performance: - - Load Regulation Calculations: - To calculate the load regulation, we need to know the input voltage range, output voltage, and the regulator's load regulation specification. Assuming a typical input voltage range of 9-12V and an output voltage of 5V, the load regulation can be estimated using the LM7805 datasheet. +MISSING SPECIFICATIONS CHECK: +- The circuit description does not provide timing requirements, voltage levels, or signal characteristics for critical signals, making a comprehensive signal integrity analysis challenging. - Load Regulation = (ΔVout / Vout) * 100% - For the LM7805, the typical load regulation is 5 mV (maximum 100 mV) for a load current change of 5 mA to 1.5A. - Load Regulation = (5 mV / 5V) * 100% = 0.1% (typical) +However, based on the available information: +- The voltage division (double_divider0) uses resistor networks, which should have minimal impact on signal integrity. +- The presence of capacitors (C1, C2, C3, C4) helps mitigate power supply noise coupling and ground bounce effects. - - Line Regulation Performance: - Line regulation is the ability of the regulator to maintain a constant output voltage despite changes in input voltage. The LM7805 datasheet specifies a typical line regulation of 10 mV (maximum 30 mV) for an input voltage change of 7V to 25V. +4. Component Analysis: - Line Regulation = (ΔVout / Vout) * 100% - Line Regulation = (10 mV / 5V) * 100% = 0.2% (typical) +CRITICAL MISSING INFORMATION CHECK: +- No missing part numbers or undefined footprints were identified. +- Tolerance specifications and temperature ratings for the components are not provided. - - Transient Response Characteristics: - The transient response characteristics, such as rise time, overshoot, and settling time, depend on the output capacitance (C3) and the regulator's transient response specifications. These characteristics should be evaluated based on the load transient requirements and regulator datasheet. +Based on the available information: +- The resistor values (R1-R6) and capacitor values (C1-C4) are specified, allowing for basic component verification. +- The LM7805 regulator (U1) is a widely used and reliable component for low-power applications. +- The diode (D1) is a general-purpose 1N4148W signal diode suitable for input protection. - - PSRR (Power Supply Rejection Ratio): - The PSRR quantifies the regulator's ability to reject input noise and ripple. The LM7805 datasheet specifies a typical PSRR of 65 dB at 1 kHz, which should be evaluated based on the expected input noise frequencies. +RECOMMENDATION: To perform a comprehensive component analysis, additional information such as tolerance specifications, temperature ratings, and reliability data (e.g., MTBF) should be provided for each component. - - Decoupling Network Design: - - Capacitor Selection and Placement: - The input capacitor (C1) is a 100 μF electrolytic capacitor, which is suitable for bulk decoupling. The output capacitors (C2, C3) are 10 μF ceramic capacitors, which provide high-frequency decoupling. However, additional high-frequency decoupling capacitors may be needed closer to the voltage dividers and other loads. +5. Reliability and Safety Analysis: - - Resonant Frequency Considerations: - The resonant frequency of the decoupling network should be evaluated to ensure it does not coincide with any critical frequencies in the system. This analysis requires knowledge of the parasitic inductances and capacitances. +CRITICAL SAFETY CHECKS: +- No specific safety-related specifications, isolation requirements, or environmental ratings are provided in the circuit description. - - ESR/ESL Impacts: - The equivalent series resistance (ESR) and equivalent series inductance (ESL) of the decoupling capacitors can impact the effectiveness of the decoupling network, especially at high frequencies. Selecting capacitors with low ESR and ESL values is recommended. +RECOMMENDATION: For a thorough reliability and safety analysis, the following information should be provided: +- Operating temperature range +- Environmental conditions (humidity, vibration, etc.) +- Intended application and potential hazards +- Isolation and protection requirements - - Voltage Drops across Power Planes and Traces: - To accurately calculate the voltage drops across power planes and traces, detailed information about trace lengths, widths, and current densities is required. However, it is essential to ensure that the voltage drops are within acceptable limits to prevent undershoots or overshoots at the loads. +Without this information, a comprehensive reliability and safety analysis cannot be performed effectively. - - Protection Mechanisms: - - Overvoltage Protection: - The circuit does not appear to have explicit overvoltage protection. Adding a Zener diode or transient voltage suppressor (TVS) diode at the input could provide overvoltage protection. - - - Overcurrent Protection: - The circuit does not have any overcurrent protection mechanism. Adding a fuse or resettable polyfuse in series with the input could provide overcurrent protection. - - - Reverse Polarity Protection: - The input protection diode (D1) provides reverse polarity protection by preventing current flow in the reverse direction. - - - Inrush Current Limiting: - The circuit does not have any inrush current limiting mechanism. Adding an NTC (Negative Temperature Coefficient) thermistor or a soft-start circuit could limit the inrush current during power-up. - - - Power Budget Analysis: - - Worst-case Power Consumption: - To calculate the worst-case power consumption, the maximum current draw of all loads must be known. This information is not provided in the circuit description. - - - Thermal Implications: - The thermal implications depend on the power dissipation of the components, especially the voltage regulator (U1) and the power resistors (R2, R3, R4, R5, R6). Proper heat sinking and thermal management should be considered. - - - Efficiency Calculations: - The efficiency of the voltage regulator can be estimated from the datasheet. However, the overall system efficiency would also depend on the power dissipation in the resistors and other components. - - - Power Sequencing Requirements: - The circuit does not appear to have any specific power sequencing requirements. However, if multiple voltage domains are present, proper power sequencing may be necessary to avoid potential issues. - -3. Detailed Signal Integrity Analysis: - - Critical Signal Paths: - - Rise/Fall Time Calculations: - To accurately calculate the rise and fall times of critical signals, detailed information about the driving and loading conditions is required. This includes the output impedance of the drivers, input capacitances of the loads, and trace lengths. - - - Propagation Delay Estimation: - Propagation delays depend on the trace lengths, propagation velocity in the PCB material, and the loading conditions. Detailed PCB layout information and component specifications are needed for accurate propagation delay estimation. - - - Loading Effects Analysis: - The loading effects on critical signals should be analyzed to ensure that the driving circuits can adequately drive the loads without excessive signal degradation. This includes the input capacitances of the loads and the impedance matching between the source and load. - - - Reflection Analysis for High-speed Signals: - If the circuit involves high-speed signals (e.g., above a few MHz), a reflection analysis should be performed to ensure that signal reflections do not cause excessive ringing or overshoot. This analysis requires knowledge of the trace impedances and termination strategies. - - - Noise Susceptibility: - - Common-mode Noise Rejection: - The circuit does not appear to have any differential signaling or common-mode noise rejection provisions. If common-mode noise is a concern, appropriate shielding, filtering, or differential signaling techniques may be required. - - - Power Supply Noise Coupling: - The decoupling capacitors (C1, C2, C3, C4) help mitigate power supply noise coupling. However, additional decoupling or filtering may be needed if high-frequency noise is present on the power rails. - - - Ground Bounce Effects: - Ground bounce effects can occur due to high current transients and shared ground impedances. Proper layout techniques, such as minimizing ground loops and using star ground topologies, can help mitigate ground bounce. - - - Cross-talk between Signals: - Cross-talk between signals can be minimized by maintaining proper spacing between traces, using ground planes as shields, and employing differential signaling techniques if necessary. - - - Signal Termination Strategies: - The output termination block (output_termination0) consists of a single 100 kΩ resistor (R6). This resistor may not provide adequate termination for high-speed signals or impedance matching, depending on the signal characteristics and trace impedances. - - - Signal Margins and Timing Budgets: - Without detailed information about the signal levels, timing requirements, and loading conditions, it is difficult to accurately calculate signal margins and timing budgets. - - - Impedance Matching Requirements: - Impedance matching is crucial for high-speed signals to minimize reflections and ensure proper signal integrity. The circuit design does not provide information about the trace impedances or termination strategies, making it challenging to evaluate impedance matching requirements. - -4. Extensive Component Analysis: - - Detailed Component Review: - - Electrical Specifications: - The circuit incorporates passive components (resistors and capacitors) and an LM7805 voltage regulator. The electrical specifications of these components should be reviewed in detail, including their voltage and current ratings, temperature coefficients, and tolerances. - - - Thermal Characteristics: - The thermal characteristics of the components, particularly the voltage regulator (U1) and power resistors (R2, R3, R4, R5, R6), should be evaluated. This includes analyzing their power dissipation, thermal resistance, and maximum operating temperatures. - - - Reliability Metrics (MTBF): - The mean time between failures (MTBF) of the components should be reviewed to estimate the overall reliability of the circuit. This information is typically available in the component datasheets or from the manufacturers. - - - Cost Considerations: - The cost of the components should be evaluated to optimize the overall bill of materials (BOM) cost while maintaining the required performance and reliability. - - - Availability and Lifecycle Status: - The availability and lifecycle status of the components should be considered to ensure a stable and long-term supply chain. End-of-life (EOL) or obsolescence issues should be addressed, if applicable. - - - Footprint and Package Analysis: - - Thermal Considerations: - The footprints and packages of the components, especially the voltage regulator (U1) and power resistors, should be evaluated for their thermal dissipation capabilities. Proper heat sinking or airflow may be required for components with high power dissipation. - - - Assembly Requirements: - The footprints and packages should be evaluated for their assembly requirements, such as solderability, lead-free compliance, and compatibility with the manufacturing processes. - - - Reliability Implications: - The package types and footprints can have implications on the reliability of the components. For example, surface-mount components generally have better vibration resistance compared to through-hole components. - - - Component Derating Analysis: - - Voltage Derating: - The applied voltages across the components should be derated to ensure a sufficient safety margin and longer component lifetimes. Typical voltage derating factors should be applied based on industry standards and component specifications. - - - Current Derating: - The current flowing through the components should be derated to prevent excessive power dissipation and ensure reliable operation. Current derating factors should be applied based on the component specifications and operating conditions. - - - Temperature Derating: - The operating temperatures of the components should be derated to account for temperature variations, self-heating effects, and to extend component lifetimes. Temperature derating factors should be applied based on the component specifications and thermal characteristics. - - - Alternative Component Recommendations: - Based on the detailed component analysis, alternative components may be recommended to improve performance, reliability, cost-effectiveness, or availability. These recommendations should consider factors such as electrical specifications, thermal characteristics, footprints, and lifecycle status. - -5. Comprehensive Reliability and Safety Analysis: - - Detailed FMEA (Failure Mode and Effects Analysis): - - Component-level Failure Modes: - Potential failure modes of each component should be identified and analyzed. This includes open-circuit failures, short-circuit failures, parametric drifts, and other failure mechanisms specific to the component types. - - - System-level Failure Impacts: - The impact of each component failure mode on the overall system functionality should be evaluated. This includes analyzing the propagation of failures and their effects on other components or subsystems. - - - Criticality Assessment: - The criticality of each failure mode should be assessed based on factors such as severity, probability of occurrence, and detectability. This assessment helps prioritize mitigation strategies and allocate resources effectively. - - - Mitigation Strategies: - Mitigation strategies should be developed for critical failure modes. These may include component redundancy, fault-tolerant design techniques, protection circuits, or diagnostic and monitoring capabilities. - - - Safety Compliance Review: - - Isolation Requirements: - If the circuit operates with high voltages or interfaces with external systems, appropriate isolation requirements should be evaluated to ensure user safety and compliance with relevant standards. +6. Manufacturing and Testing Considerations: - - Clearance and Creepage Distances: - The clearance and creepage distances between conductive parts should be reviewed to prevent electrical arcing or tracking, especially in high-voltage or high-pollution environments. +MISSING MANUFACTURING INFORMATION CHECK: +- No assembly requirements, test specifications, or calibration requirements are provided in the circuit description. - - Protection against Electrical Hazards: - Appropriate protection mechanisms, such as fusing, current limiting, and insulation barriers, should be implemented to mitigate electrical hazards and ensure user safety. +RECOMMENDATION: To evaluate manufacturing and testing considerations, the following information should be provided: +- Assembly process requirements (e.g., reflow soldering, wave soldering) +- Access points for programming, debugging, and testing +- Functional test coverage requirements +- Calibration procedures (if applicable) +- Quality control and yield optimization strategies - - Thermal Safety Considerations: - The thermal safety aspects of the circuit should be evaluated, including component operating temperatures, touch temperatures, and potential fire hazards. Proper thermal management and insulation techniques may be required. +7. Regulatory and Standards Compliance: - - Environmental Considerations: - - Temperature Range Analysis: - The circuit should be designed to operate reliably within the specified temperature range. Derating factors and appropriate component selections should be applied based on the temperature range requirements. +MISSING COMPLIANCE INFORMATION CHECK: +- No information regarding applicable safety standards, environmental regulations, or industry-specific standards is provided in the circuit description. - - Humidity Effects: - The effects of humidity on the circuit components and interconnections should be evaluated. Conformal coatings, moisture-resistant materials, or hermetic sealing may be necessary in high-humidity environments. +RECOMMENDATION: To assess regulatory and standards compliance, the following information should be provided: +- Intended application and operating environment +- Applicable safety standards (e.g., UL, CE) +- Environmental regulations (e.g., RoHS, REACH) +- Industry-specific standards or certifications required - - Vibration Resistance: - If the circuit is subjected to vibrations or mechanical shocks, the component footprints, interconnections, and mechanical mounting should be designed to withstand these environmental conditions. +8. Practical Implementation Review: - - EMI/EMC Compliance: - The circuit should be evaluated for electromagnetic interference (EMI) and electromagnetic compatibility (EMC) compliance. Appropriate shielding, filtering, and layout techniques may be required to mitigate EMI/EMC issues. +MISSING PRACTICAL INFORMATION CHECK: +- No mounting requirements, connector specifications, or cooling requirements are provided in the circuit description. - - Reliability Calculations and Predictions: - Based on the component reliability data and failure mode analysis, reliability calculations and predictions should be performed for the overall circuit. This may involve techniques such as Reliability Block Diagrams (RBDs), Fault Tree Analysis (FTA), or Monte Carlo simulations. +RECOMMENDATION: To evaluate practical implementation considerations, the following information should be provided: +- Enclosure or mounting requirements +- Connector types and accessibility +- Cooling needs (e.g., heatsinking, airflow) +- Operating environment conditions (temperature, humidity, dust/water protection) +- Field maintenance and service access requirements -6. Manufacturing and Testing Considerations: - - DFM (Design for Manufacturing) Analysis: - - Component Placement Optimization: - The component placement on the PCB should be optimized for manufacturing considerations, such as pick-and-place machine accessibility, thermal management, and signal integrity. +9. Documentation and Specifications: - - Assembly Process Requirements: - The circuit design should be reviewed for compatibility with the intended assembly processes, such as wave soldering, reflow soldering, or hand assembly. Component footprints, clearances, and thermal profiles should be considered. +MISSING DOCUMENTATION CHECK: +- No design specifications, interface control documents, test procedures, or user manuals are provided. - - Test Point Accessibility: - Appropriate test points should be included in the design to facilitate testing and debugging during manufacturing and field service. The accessibility of these test points should be evaluated. +RECOMMENDATION: To ensure proper documentation and specifications, the following should be provided or developed: +- Design specifications with detailed operating parameters and performance requirements +- Interface control documents defining signal characteristics and protocols +- Test procedures for verification and validation +- User manuals and service documentation - - Programming/Debugging Access: - If the circuit includes programmable devices or microcontrollers, provisions for programming and debugging access should be made, such as dedicated programming headers or in-circuit programming interfaces. +Additional Analysis Requirements: - - Test Strategy Recommendations: - - Functional Test Coverage: - A comprehensive functional test strategy should be developed to ensure thorough testing of the circuit's functionality, including edge cases and corner conditions. +If there are any additional specific requirements or constraints for the analysis, please provide them, and I will incorporate them into the review. - - In-circuit Test Requirements: - In-circuit testing requirements should be evaluated, such \ No newline at end of file +Please note that while this analysis aims to be comprehensive, the lack of certain critical information may limit the depth of the review in some areas. Providing the missing information highlighted in the recommendations would enable a more detailed and accurate analysis. \ No newline at end of file diff --git a/skidl_test.py b/skidl_llm_test.py similarity index 92% rename from skidl_test.py rename to skidl_llm_test.py index 2d3b7a155..05fdb4601 100644 --- a/skidl_test.py +++ b/skidl_llm_test.py @@ -97,7 +97,14 @@ def complete_circuit(): analyzer = SkidlCircuitAnalyzer( provider="anthropic", api_key=os.getenv("ANTHROPIC_API_KEY"), - model="claude-3-sonnet-20240229" + model="claude-3-sonnet-20240229", + custom_prompt="Additional specific requirements...", + analysis_flags={ + "design_review": True, + "power_analysis": False, # Disable sections you don't need + "signal_integrity": True, + # ... other flags + } ) # # Using OpenAI diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index 486989957..e69977719 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -13,6 +13,8 @@ def __init__( provider: str = "anthropic", api_key: Optional[str] = None, model: Optional[str] = None, + custom_prompt: Optional[str] = None, + analysis_flags: Optional[Dict[str, bool]] = None, **kwargs ): """ @@ -22,106 +24,126 @@ def __init__( provider: LLM provider to use ("anthropic", "openai", or "openrouter") api_key: API key for the chosen provider model: Specific model to use (provider-dependent) + custom_prompt: Optional custom prompt to append to the base analysis prompt + analysis_flags: Dictionary to enable/disable specific analysis sections **kwargs: Additional provider-specific configuration """ self.provider = get_provider(provider, api_key) self.model = model + self.custom_prompt = custom_prompt + self.analysis_flags = analysis_flags or { + "design_review": True, + "power_analysis": True, + "signal_integrity": True, + "component_analysis": True, + "reliability_safety": True, + "manufacturing": True, + "compliance": True, + "documentation": True, + "practical_implementation": True + } self.config = kwargs def _generate_analysis_prompt(self, circuit_description: str) -> str: """Generate structured prompt for circuit analysis.""" - prompt = f""" -Please analyze this electronic circuit design as an expert electronics engineer. -Provide an extremely detailed technical analysis with specific calculations, thorough explanations, -and comprehensive recommendations. Focus on practical improvements for reliability, safety, and performance. - -Circuit Description: -{circuit_description} - -Please provide an extensive analysis covering each of these areas in great detail: - + + # Base prompt sections + sections = { + "design_review": """ 1. Comprehensive Design Architecture Review: -- Evaluate the overall hierarchical structure and design patterns used -- Assess modularity, reusability, and maintainability of each block -- Analyze interface definitions and protocols between blocks +- Evaluate overall hierarchical structure and design patterns +- Assess modularity, reusability, and maintainability +- Analyze interface definitions and protocols - Review signal flow and control paths -- Evaluate design scalability and future expansion capabilities -- Identify potential bottlenecks in the architecture +- Evaluate design scalability +- Identify potential bottlenecks +- CHECK FOR MISSING INFORMATION: Document any undefined pin functions, missing part values, or unclear connections +- DESIGN COMPLETENESS: Verify all necessary subsystems are present for intended functionality""", + "power_analysis": """ 2. In-depth Power Distribution Analysis: -- Map complete power distribution network from input to all loads -- Analyze voltage regulation performance including: - * Load regulation calculations - * Line regulation performance +- Map complete power distribution network +- Calculate voltage drops and power dissipation +- Verify power supply requirements and ratings +- MISSING SPECIFICATIONS CHECK: Flag any components missing voltage ratings or power specifications +- Analyze voltage regulation performance: + * Load/line regulation calculations * Transient response characteristics - * PSRR at various frequencies -- Evaluate decoupling network design: - * Capacitor selection and placement - * Resonant frequency considerations - * ESR/ESL impacts -- Calculate voltage drops across power planes and traces -- Review protection mechanisms: - * Overvoltage protection - * Overcurrent protection - * Reverse polarity protection - * Inrush current limiting -- Perform power budget analysis including: - * Worst-case power consumption - * Thermal implications - * Efficiency calculations - * Power sequencing requirements + * PSRR analysis + * Thermal considerations +- Protection mechanisms analysis: + * Overvoltage/undervoltage + * Overcurrent/short circuit + * Reverse polarity + * Inrush current +- Power sequencing requirements +- Efficiency calculations +- EMI/EMC considerations for power distribution""", + "signal_integrity": """ 3. Detailed Signal Integrity Analysis: - Analyze all critical signal paths: * Rise/fall time calculations * Propagation delay estimation - * Loading effects analysis - * Reflection analysis for high-speed signals + * Loading effects + * Cross-talk potential +- MISSING SPECIFICATIONS CHECK: Identify signals missing timing requirements or voltage levels - Evaluate noise susceptibility: * Common-mode noise rejection * Power supply noise coupling * Ground bounce effects - * Cross-talk between signals -- Review signal termination strategies - Calculate signal margins and timing budgets - Assess impedance matching requirements +- Review termination strategies""", + "component_analysis": """ 4. Extensive Component Analysis: -- Detailed review of each component: - * Electrical specifications - * Thermal characteristics +- CRITICAL MISSING INFORMATION CHECK: + * Flag missing part numbers + * Identify components lacking tolerance specifications + * Note missing temperature ratings + * Check for undefined footprints +- Detailed component review: + * Voltage/current ratings + * Operating temperature range * Reliability metrics (MTBF) * Cost considerations * Availability and lifecycle status -- Footprint and package analysis: - * Thermal considerations - * Assembly requirements - * Reliability implications -- Component derating analysis: - * Voltage derating - * Current derating - * Temperature derating - Alternative component recommendations +- Verify component compatibility: + * Voltage levels + * Logic families + * Load capabilities +- Second-source availability +- End-of-life status check +- Cost optimization opportunities""", + "reliability_safety": """ 5. Comprehensive Reliability and Safety Analysis: -- Detailed FMEA (Failure Mode and Effects Analysis): - * Component-level failure modes - * System-level failure impacts - * Criticality assessment - * Mitigation strategies -- Safety compliance review: - * Isolation requirements - * Clearance and creepage distances - * Protection against electrical hazards - * Thermal safety considerations +- CRITICAL SAFETY CHECKS: + * Identify missing safety-related specifications + * Flag undefined isolation requirements + * Note missing environmental ratings +- Detailed FMEA (Failure Mode and Effects Analysis) - Environmental considerations: * Temperature range analysis * Humidity effects * Vibration resistance - * EMI/EMC compliance + * Protection rating requirements +- Safety compliance review: + * Isolation requirements + * Clearance and creepage distances + * Protection against electrical hazards + * Thermal safety - Reliability calculations and predictions +- Risk assessment matrix""", + "manufacturing": """ 6. Manufacturing and Testing Considerations: +- MISSING MANUFACTURING INFORMATION CHECK: + * Flag undefined assembly requirements + * Identify missing test specifications + * Note unclear calibration requirements - DFM (Design for Manufacturing) analysis: * Component placement optimization * Assembly process requirements @@ -132,40 +154,101 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: * In-circuit test requirements * Boundary scan capabilities * Built-in self-test features -- Production cost optimization suggestions +- Production cost optimization - Quality control recommendations +- Yield optimization strategies""", -7. Detailed Technical Recommendations: -- Prioritized list of critical improvements needed -- Specific component value optimization calculations -- Alternative design approaches with trade-off analysis -- Performance enhancement suggestions with quantitative benefits -- Reliability improvement recommendations -- Cost reduction opportunities -- Maintenance and serviceability improvements - -8. Compliance and Standards Review: -- Industry standard compliance analysis -- Regulatory requirements review -- Design guideline conformance -- Best practices comparison + "compliance": """ +7. Regulatory and Standards Compliance: +- MISSING COMPLIANCE INFORMATION CHECK: + * Flag undefined safety requirements + * Identify missing environmental compliance needs + * Note unclear certification requirements +- Industry standard compliance analysis: + * Safety standards (UL, CE, etc.) + * EMC requirements + * Environmental regulations (RoHS, REACH) + * Industry-specific standards - Documentation requirements - Certification considerations +- Testing requirements for compliance +- Required safety margins""", + + "practical_implementation": """ +8. Practical Implementation Review: +- MISSING PRACTICAL INFORMATION CHECK: + * Flag undefined mounting requirements + * Identify missing connector specifications + * Note unclear cooling requirements +- Installation considerations: + * Mounting requirements + * Cooling needs + * Connector accessibility + * Service access +- Field maintenance requirements: + * Calibration needs + * Preventive maintenance + * Repair accessibility +- Operating environment considerations: + * Temperature extremes + * Humidity ranges + * Dust/water protection + * Vibration resistance""", + + "documentation": """ +9. Documentation and Specifications: +- MISSING DOCUMENTATION CHECK: + * Flag undefined operating parameters + * Identify missing interface specifications + * Note unclear performance requirements +- Required documentation: + * Design specifications + * Interface control documents + * Test procedures + * User manuals + * Service documentation +- Design history and decisions +- Change control requirements +- Version control needs""" + } + + # Build the prompt based on enabled analysis flags + enabled_sections = [] + for section, content in sections.items(): + if self.analysis_flags.get(section, True): + enabled_sections.append(content) + + base_prompt = f""" +As an expert electronics engineer, please provide a detailed professional analysis of this circuit design. +Focus on practical implementation considerations, safety requirements, and manufacturing readiness. +Flag any missing critical information that would be needed for professional implementation. -For each section, provide: -- Detailed technical analysis -- Specific calculations where applicable -- Quantitative assessments -- Priority rankings for issues found -- Concrete improvement recommendations -- Cost-benefit analysis of suggested changes -- Risk assessment of identified issues +Circuit Description: +{circuit_description} + +{'\n'.join(enabled_sections)} + +For each section: +- Provide detailed technical analysis with specific calculations where applicable +- Flag any missing critical information needed for professional implementation +- Include specific recommendations with justification +- Prioritize issues found (Critical/High/Medium/Low) +- Include relevant industry standards and best practices +- Provide specific action items to address identified issues -Use industry standard terminology and provide specific technical details throughout the analysis. -Reference relevant standards, typical values, and best practices where applicable. -Support all recommendations with technical reasoning and specific benefits. +Presentation format for each issue: +SEVERITY: (Critical/High/Medium/Low) +DESCRIPTION: Clear description of the issue +IMPACT: Potential consequences +RECOMMENDATION: Specific action items +STANDARDS: Relevant industry standards or best practices """ - return prompt + + # Append custom prompt if provided + if self.custom_prompt: + base_prompt += f"\n\nAdditional Analysis Requirements:\n{self.custom_prompt}" + + return base_prompt def analyze_circuit( self, @@ -229,7 +312,8 @@ def analyze_circuit( "request_time_seconds": request_time, "prompt_tokens": prompt_tokens, "response_tokens": analysis_tokens, - "total_time_seconds": time.time() - start_time + "total_time_seconds": time.time() - start_time, + "enabled_analyses": [k for k, v in self.analysis_flags.items() if v] } if verbose: diff --git a/src/skidl/llm_providers.py b/src/skidl/llm_providers.py index adc45ca7c..1afeb398c 100644 --- a/src/skidl/llm_providers.py +++ b/src/skidl/llm_providers.py @@ -7,6 +7,7 @@ from openai import OpenAI from anthropic import Anthropic import requests +from datetime import datetime class LLMProvider(ABC): """Abstract base class for LLM providers.""" @@ -38,7 +39,7 @@ def generate_analysis(self, prompt: str, **kwargs) -> Dict: try: # Use the specified model or default to claude-3-sonnet model = kwargs.get('model') or "claude-3-sonnet-20240229" - max_tokens = kwargs.get('max_tokens', 8000) + max_tokens = kwargs.get('max_tokens', 12000) # Increased from 8000 response = self.client.messages.create( model=model, @@ -49,8 +50,6 @@ def generate_analysis(self, prompt: str, **kwargs) -> Dict: }] ) - # Use current timestamp since Anthropic response doesn't include it - from datetime import datetime return { "success": True, "analysis": response.content[0].text, From 6bd9d5901d556c207de2abf3b1889b0e208e0327 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 19:09:53 -0800 Subject: [PATCH 10/73] improved --- circuit_llm_analysis.txt | 143 +++-------- src/skidl/circuit_analyzer.py | 460 +++++++++++++++++++--------------- 2 files changed, 297 insertions(+), 306 deletions(-) diff --git a/circuit_llm_analysis.txt b/circuit_llm_analysis.txt index a8f9dc24c..3cdbe0542 100644 --- a/circuit_llm_analysis.txt +++ b/circuit_llm_analysis.txt @@ -1,108 +1,35 @@ -As an expert electronics engineer, I'm happy to provide a comprehensive professional analysis of the given circuit design. Please note that this analysis will be based solely on the information provided in the circuit description, and any additional requirements or constraints should be clearly specified. - -1. Overall Design Architecture Review: - -- The hierarchical structure of the circuit is well-organized, with subsystems logically grouped into modules (e.g., double_divider0, power_section0). -- The modular approach promotes reusability and maintainability, as individual sections can be replaced or updated without affecting the entire system. -- The signal flow and control paths are well-defined, with clear connectivity between modules and components. -- The design appears to be scalable, as additional components or subsystems can be integrated into the existing hierarchy. - -MISSING INFORMATION CHECK: -- No undefined pin functions or missing part values were identified in the provided information. -- All necessary subsystems for the intended functionality (voltage regulation, voltage division, input protection, and filtering) appear to be present. - -2. Power Supply Design Review: - -- The input protection section (input_protection0) includes a diode (D1) for reverse polarity protection and a bulk capacitor (C1) for filtering high-frequency noise. -- The voltage regulator section (voltage_regulator0) uses an LM7805 linear regulator (U1) with appropriate input and output capacitors (C2, C3) for stability and ripple reduction. -- The RC filter section (rc_filter0) provides additional high-frequency noise filtering on the regulated output using a resistor (R1) and capacitor (C4). - -MISSING INFORMATION CHECK: -- No missing information regarding the power supply design was identified. - -3. Signal Integrity Analysis: - -MISSING SPECIFICATIONS CHECK: -- The circuit description does not provide timing requirements, voltage levels, or signal characteristics for critical signals, making a comprehensive signal integrity analysis challenging. - -However, based on the available information: -- The voltage division (double_divider0) uses resistor networks, which should have minimal impact on signal integrity. -- The presence of capacitors (C1, C2, C3, C4) helps mitigate power supply noise coupling and ground bounce effects. - -4. Component Analysis: - -CRITICAL MISSING INFORMATION CHECK: -- No missing part numbers or undefined footprints were identified. -- Tolerance specifications and temperature ratings for the components are not provided. - -Based on the available information: -- The resistor values (R1-R6) and capacitor values (C1-C4) are specified, allowing for basic component verification. -- The LM7805 regulator (U1) is a widely used and reliable component for low-power applications. -- The diode (D1) is a general-purpose 1N4148W signal diode suitable for input protection. - -RECOMMENDATION: To perform a comprehensive component analysis, additional information such as tolerance specifications, temperature ratings, and reliability data (e.g., MTBF) should be provided for each component. - -5. Reliability and Safety Analysis: - -CRITICAL SAFETY CHECKS: -- No specific safety-related specifications, isolation requirements, or environmental ratings are provided in the circuit description. - -RECOMMENDATION: For a thorough reliability and safety analysis, the following information should be provided: -- Operating temperature range -- Environmental conditions (humidity, vibration, etc.) -- Intended application and potential hazards -- Isolation and protection requirements - -Without this information, a comprehensive reliability and safety analysis cannot be performed effectively. - -6. Manufacturing and Testing Considerations: - -MISSING MANUFACTURING INFORMATION CHECK: -- No assembly requirements, test specifications, or calibration requirements are provided in the circuit description. - -RECOMMENDATION: To evaluate manufacturing and testing considerations, the following information should be provided: -- Assembly process requirements (e.g., reflow soldering, wave soldering) -- Access points for programming, debugging, and testing -- Functional test coverage requirements -- Calibration procedures (if applicable) -- Quality control and yield optimization strategies - -7. Regulatory and Standards Compliance: - -MISSING COMPLIANCE INFORMATION CHECK: -- No information regarding applicable safety standards, environmental regulations, or industry-specific standards is provided in the circuit description. - -RECOMMENDATION: To assess regulatory and standards compliance, the following information should be provided: -- Intended application and operating environment -- Applicable safety standards (e.g., UL, CE) -- Environmental regulations (e.g., RoHS, REACH) -- Industry-specific standards or certifications required - -8. Practical Implementation Review: - -MISSING PRACTICAL INFORMATION CHECK: -- No mounting requirements, connector specifications, or cooling requirements are provided in the circuit description. - -RECOMMENDATION: To evaluate practical implementation considerations, the following information should be provided: -- Enclosure or mounting requirements -- Connector types and accessibility -- Cooling needs (e.g., heatsinking, airflow) -- Operating environment conditions (temperature, humidity, dust/water protection) -- Field maintenance and service access requirements - -9. Documentation and Specifications: - -MISSING DOCUMENTATION CHECK: -- No design specifications, interface control documents, test procedures, or user manuals are provided. - -RECOMMENDATION: To ensure proper documentation and specifications, the following should be provided or developed: -- Design specifications with detailed operating parameters and performance requirements -- Interface control documents defining signal characteristics and protocols -- Test procedures for verification and validation -- User manuals and service documentation - -Additional Analysis Requirements: - -If there are any additional specific requirements or constraints for the analysis, please provide them, and I will incorporate them into the review. - -Please note that while this analysis aims to be comprehensive, the lack of certain critical information may limit the depth of the review in some areas. Providing the missing information highlighted in the recommendations would enable a more detailed and accurate analysis. \ No newline at end of file +Thank you for providing the detailed circuit description and clear analysis requirements. To provide a comprehensive and accurate assessment, I will need additional critical information that was not included in the provided details. Please furnish the following information to enable a thorough professional analysis: + +1. System Overview: + - Primary function and purpose of the circuit + - Target application domain + - Required performance specifications (e.g., speed, accuracy, resolution) + - Operating environment details (e.g., industrial, consumer, automotive) + +2. Electrical Specifications: + - Input voltage specifications (range, ripple, regulation) + - Output voltage and current requirements + - Power budget and efficiency targets + - Signal specifications (levels, timing, protocols) if applicable + +3. Environmental Requirements: + - Operating temperature range + - Humidity requirements + - Vibration/shock specifications + - IP rating requirements + +4. Regulatory Requirements: + - Required certifications (UL, CE, etc.) + - EMC/EMI requirements + - Safety standards + - Industry-specific regulations + +5. Manufacturing/Cost Targets: + - Production volume estimates + - Target BOM cost + - Assembly requirements + - Testing requirements + +With this additional information, I can provide a comprehensive analysis covering all the requested aspects, including system-level integration, signal integrity, thermal performance, noise analysis, testing strategy, and specific recommendations for improvement. + +Please provide as much of this critical information as possible to enable an accurate and actionable analysis. If any of the requested details are not available or applicable, kindly let me know. \ No newline at end of file diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index e69977719..bd562f6d6 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -1,15 +1,11 @@ -"""Module for analyzing SKIDL circuits using various LLM providers.""" - from typing import Dict, Optional, List from datetime import datetime import time from .llm_providers import get_provider class SkidlCircuitAnalyzer: - """Analyzes SKIDL circuits using various LLM providers.""" - def __init__( - self, + self, provider: str = "anthropic", api_key: Optional[str] = None, model: Optional[str] = None, @@ -17,21 +13,13 @@ def __init__( analysis_flags: Optional[Dict[str, bool]] = None, **kwargs ): - """ - Initialize the circuit analyzer. - - Args: - provider: LLM provider to use ("anthropic", "openai", or "openrouter") - api_key: API key for the chosen provider - model: Specific model to use (provider-dependent) - custom_prompt: Optional custom prompt to append to the base analysis prompt - analysis_flags: Dictionary to enable/disable specific analysis sections - **kwargs: Additional provider-specific configuration - """ + """Initialize with same parameters as original""" self.provider = get_provider(provider, api_key) self.model = model self.custom_prompt = custom_prompt self.analysis_flags = analysis_flags or { + "system_overview": True, + "subcircuit_analysis": True, "design_review": True, "power_analysis": True, "signal_integrity": True, @@ -40,209 +28,284 @@ def __init__( "manufacturing": True, "compliance": True, "documentation": True, - "practical_implementation": True + "practical_implementation": True, + "thermal_analysis": True, + "noise_analysis": True, + "testing_verification": True } self.config = kwargs - - def _generate_analysis_prompt(self, circuit_description: str) -> str: - """Generate structured prompt for circuit analysis.""" - - # Base prompt sections - sections = { + + def _generate_system_requirements_prompt(self) -> str: + """Generate prompt section for system requirements""" + return """ +REQUIRED SYSTEM INFORMATION: +Please provide the following critical information for accurate analysis: + +1. System Overview: +- Primary function and purpose of the circuit +- Target application domain +- Required performance specifications +- Operating environment details + +2. Electrical Specifications: +- Input voltage specifications (range, ripple, regulation) +- Output requirements (voltage, current, regulation) +- Power budget and efficiency targets +- Signal specifications (levels, timing, protocols) + +3. Environmental Requirements: +- Operating temperature range +- Humidity requirements +- Vibration/shock specifications +- IP rating requirements + +4. Regulatory Requirements: +- Required certifications (UL, CE, etc.) +- EMC/EMI requirements +- Safety standards +- Industry-specific regulations + +5. Manufacturing/Cost Targets: +- Production volume estimates +- Target BOM cost +- Assembly requirements +- Testing requirements + +Please provide as much of this information as possible to enable comprehensive analysis.""" + + def _generate_subcircuit_analysis_prompt(self) -> str: + """Generate prompt for subcircuit analysis""" + return """ +SUBCIRCUIT ANALYSIS REQUIREMENTS: + +For each identified subcircuit, provide detailed analysis covering: + +1. Functional Analysis: +- Primary purpose and operation +- Input/output specifications +- Critical parameters and constraints +- Performance requirements +- Integration with other subcircuits + +2. Component-Level Review: +- Critical component specifications +- Operating point analysis +- Tolerance analysis +- Worst-case scenario analysis +- Component interaction effects + +3. Performance Analysis: +- Transfer function analysis +- Frequency response +- Stability analysis +- Temperature effects +- Power consumption + +4. Failure Mode Analysis: +- Single point failure analysis +- Cascade failure potential +- Protection mechanisms +- Recovery mechanisms +- Reliability projections + +5. Implementation Considerations: +- Layout requirements +- Thermal considerations +- EMI/EMC requirements +- Test point access +- Debug capabilities + +Analyze each subcircuit individually before assessing system-level interactions.""" + + def _generate_analysis_sections(self) -> Dict[str, str]: + """Generate all analysis section prompts""" + return { + "system_overview": """ +0. System-Level Analysis: +- Comprehensive system architecture review +- Interface analysis between major blocks +- System-level timing and synchronization +- Resource allocation and optimization +- System-level failure modes +- Integration challenges +- Performance bottlenecks +- Scalability assessment""", + "design_review": """ 1. Comprehensive Design Architecture Review: -- Evaluate overall hierarchical structure and design patterns -- Assess modularity, reusability, and maintainability -- Analyze interface definitions and protocols -- Review signal flow and control paths -- Evaluate design scalability -- Identify potential bottlenecks -- CHECK FOR MISSING INFORMATION: Document any undefined pin functions, missing part values, or unclear connections -- DESIGN COMPLETENESS: Verify all necessary subsystems are present for intended functionality""", +- Evaluate overall hierarchical structure +- Assess modularity and reusability +- Interface protocols analysis +- Control path verification +- Design pattern evaluation +- Critical path analysis +- Feedback loop stability +- Clock domain analysis +- Reset strategy review +- State machine verification +- Resource utilization assessment +- Design rule compliance""", "power_analysis": """ 2. In-depth Power Distribution Analysis: -- Map complete power distribution network -- Calculate voltage drops and power dissipation -- Verify power supply requirements and ratings -- MISSING SPECIFICATIONS CHECK: Flag any components missing voltage ratings or power specifications -- Analyze voltage regulation performance: - * Load/line regulation calculations - * Transient response characteristics - * PSRR analysis - * Thermal considerations -- Protection mechanisms analysis: - * Overvoltage/undervoltage - * Overcurrent/short circuit - * Reverse polarity - * Inrush current +- Complete power tree mapping +- Voltage drop calculations +- Current distribution analysis - Power sequencing requirements -- Efficiency calculations -- EMI/EMC considerations for power distribution""", +- Brownout behavior analysis +- Load transient response +- Power supply rejection ratio +- Efficiency optimization +- Thermal implications +- Battery life calculations (if applicable) +- Power integrity simulation +- Decoupling strategy +- Ground bounce analysis""", "signal_integrity": """ 3. Detailed Signal Integrity Analysis: -- Analyze all critical signal paths: - * Rise/fall time calculations - * Propagation delay estimation - * Loading effects - * Cross-talk potential -- MISSING SPECIFICATIONS CHECK: Identify signals missing timing requirements or voltage levels -- Evaluate noise susceptibility: - * Common-mode noise rejection - * Power supply noise coupling - * Ground bounce effects -- Calculate signal margins and timing budgets -- Assess impedance matching requirements -- Review termination strategies""", - - "component_analysis": """ -4. Extensive Component Analysis: -- CRITICAL MISSING INFORMATION CHECK: - * Flag missing part numbers - * Identify components lacking tolerance specifications - * Note missing temperature ratings - * Check for undefined footprints -- Detailed component review: - * Voltage/current ratings - * Operating temperature range - * Reliability metrics (MTBF) - * Cost considerations - * Availability and lifecycle status -- Alternative component recommendations -- Verify component compatibility: - * Voltage levels - * Logic families - * Load capabilities -- Second-source availability -- End-of-life status check -- Cost optimization opportunities""", - - "reliability_safety": """ -5. Comprehensive Reliability and Safety Analysis: -- CRITICAL SAFETY CHECKS: - * Identify missing safety-related specifications - * Flag undefined isolation requirements - * Note missing environmental ratings -- Detailed FMEA (Failure Mode and Effects Analysis) -- Environmental considerations: - * Temperature range analysis - * Humidity effects - * Vibration resistance - * Protection rating requirements -- Safety compliance review: - * Isolation requirements - * Clearance and creepage distances - * Protection against electrical hazards - * Thermal safety -- Reliability calculations and predictions -- Risk assessment matrix""", - - "manufacturing": """ -6. Manufacturing and Testing Considerations: -- MISSING MANUFACTURING INFORMATION CHECK: - * Flag undefined assembly requirements - * Identify missing test specifications - * Note unclear calibration requirements -- DFM (Design for Manufacturing) analysis: - * Component placement optimization - * Assembly process requirements - * Test point accessibility - * Programming/debugging access -- Test strategy recommendations: - * Functional test coverage - * In-circuit test requirements - * Boundary scan capabilities - * Built-in self-test features -- Production cost optimization -- Quality control recommendations -- Yield optimization strategies""", - - "compliance": """ -7. Regulatory and Standards Compliance: -- MISSING COMPLIANCE INFORMATION CHECK: - * Flag undefined safety requirements - * Identify missing environmental compliance needs - * Note unclear certification requirements -- Industry standard compliance analysis: - * Safety standards (UL, CE, etc.) - * EMC requirements - * Environmental regulations (RoHS, REACH) - * Industry-specific standards -- Documentation requirements -- Certification considerations -- Testing requirements for compliance -- Required safety margins""", - - "practical_implementation": """ -8. Practical Implementation Review: -- MISSING PRACTICAL INFORMATION CHECK: - * Flag undefined mounting requirements - * Identify missing connector specifications - * Note unclear cooling requirements -- Installation considerations: - * Mounting requirements - * Cooling needs - * Connector accessibility - * Service access -- Field maintenance requirements: - * Calibration needs - * Preventive maintenance - * Repair accessibility -- Operating environment considerations: - * Temperature extremes - * Humidity ranges - * Dust/water protection - * Vibration resistance""", - - "documentation": """ -9. Documentation and Specifications: -- MISSING DOCUMENTATION CHECK: - * Flag undefined operating parameters - * Identify missing interface specifications - * Note unclear performance requirements -- Required documentation: - * Design specifications - * Interface control documents - * Test procedures - * User manuals - * Service documentation -- Design history and decisions -- Change control requirements -- Version control needs""" - } +- Critical path timing analysis +- Setup/hold time verification +- Clock skew analysis +- Propagation delay calculations +- Cross-talk assessment +- Reflection analysis +- EMI/EMC considerations +- Signal loading effects +- Impedance matching +- Common mode noise rejection +- Ground loop analysis +- Shield effectiveness""", - # Build the prompt based on enabled analysis flags - enabled_sections = [] - for section, content in sections.items(): - if self.analysis_flags.get(section, True): - enabled_sections.append(content) + "thermal_analysis": """ +4. Thermal Performance Analysis: +- Component temperature rise calculations +- Thermal resistance analysis +- Heat spreading patterns +- Cooling requirements +- Thermal gradient mapping +- Hot spot identification +- Thermal cycling effects +- Temperature derating +- Thermal protection mechanisms +- Cooling solution optimization""", + "noise_analysis": """ +5. Comprehensive Noise Analysis: +- Noise source identification +- Noise coupling paths +- Ground noise analysis +- Power supply noise +- Digital switching noise +- RF interference +- Common mode noise +- Differential mode noise +- Shielding effectiveness +- Filter performance +- Noise margin calculations""", + + "testing_verification": """ +6. Testing and Verification Strategy: +- Functional test coverage +- Performance verification +- Environmental testing +- Reliability testing +- Safety verification +- EMC/EMI testing +- Production test strategy +- Self-test capabilities +- Calibration requirements +- Diagnostic capabilities +- Test point access +- Debug interface requirements""" + } + + def _generate_analysis_prompt(self, circuit_description: str) -> str: + """Generate enhanced structured prompt for circuit analysis""" + + # Get all section prompts + sections = self._generate_analysis_sections() + + # Build base prompt base_prompt = f""" -As an expert electronics engineer, please provide a detailed professional analysis of this circuit design. -Focus on practical implementation considerations, safety requirements, and manufacturing readiness. -Flag any missing critical information that would be needed for professional implementation. +You are an expert electronics engineer conducting a thorough professional analysis of a circuit design. +Your goal is to provide actionable insights and identify potential issues before implementation. + +{self._generate_system_requirements_prompt()} Circuit Description: {circuit_description} -{'\n'.join(enabled_sections)} +{self._generate_subcircuit_analysis_prompt()} + +ANALYSIS METHODOLOGY: +1. Begin with subcircuit identification and individual analysis +2. Analyze interactions between subcircuits +3. Evaluate system-level performance and integration +4. Assess manufacturing and practical implementation considerations -For each section: -- Provide detailed technical analysis with specific calculations where applicable -- Flag any missing critical information needed for professional implementation -- Include specific recommendations with justification -- Prioritize issues found (Critical/High/Medium/Low) -- Include relevant industry standards and best practices -- Provide specific action items to address identified issues +REQUIRED ANALYSIS SECTIONS:""" + + # Add enabled analysis sections + for section, content in sections.items(): + if self.analysis_flags.get(section, True): + base_prompt += f"\n{content}" -Presentation format for each issue: + # Add analysis requirements + base_prompt += """ + +For each analysis section: +1. Start with critical missing information identification +2. Provide detailed technical analysis with calculations +3. Include specific numerical criteria and measurements +4. Reference relevant industry standards +5. Provide concrete recommendations +6. Prioritize findings by severity +7. Include specific action items + +For each identified issue: SEVERITY: (Critical/High/Medium/Low) -DESCRIPTION: Clear description of the issue -IMPACT: Potential consequences -RECOMMENDATION: Specific action items -STANDARDS: Relevant industry standards or best practices -""" +CATEGORY: (Design/Performance/Safety/Manufacturing/etc.) +SUBCIRCUIT: Affected subcircuit or system level +DESCRIPTION: Detailed issue description +IMPACT: Quantified impact on system performance +VERIFICATION: How to verify the issue exists +RECOMMENDATION: Specific action items with justification +STANDARDS: Applicable industry standards +TRADE-OFFS: Impact of proposed changes +PRIORITY: Implementation priority level + +Special Requirements: +- Analyze each subcircuit completely before moving to system-level analysis +- Provide specific component recommendations where applicable +- Include calculations and formulas used in analysis +- Reference specific standards and requirements +- Consider worst-case scenarios +- Evaluate corner cases +- Assess impact of component variations +- Consider environmental effects +- Evaluate aging effects +- Assess maintenance requirements + +Output Format: +1. Executive Summary +2. Critical Findings Summary +3. Detailed Subcircuit Analysis (one section per subcircuit) +4. System-Level Analysis +5. Cross-Cutting Concerns +6. Recommendations Summary +7. Required Action Items (prioritized) +8. Additional Information Needed + +Remember to: +- Be specific and quantitative where possible +- Include calculations and methodology +- Reference specific standards +- Provide actionable recommendations +- Consider practical implementation +- Evaluate cost implications +- Assess manufacturing feasibility +- Consider maintenance requirements""" # Append custom prompt if provided if self.custom_prompt: @@ -250,6 +313,7 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: return base_prompt + # Rest of the class implementation remains the same... def analyze_circuit( self, circuit_description: str, From b25edd8e56d9071a04c0a221259acbae335b1373 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 19:13:14 -0800 Subject: [PATCH 11/73] improve prompt --- circuit_llm_analysis.txt | 194 ++++++++++++++++++++++++++++------ src/skidl/circuit_analyzer.py | 47 ++++++-- 2 files changed, 195 insertions(+), 46 deletions(-) diff --git a/circuit_llm_analysis.txt b/circuit_llm_analysis.txt index 3cdbe0542..62d554a5c 100644 --- a/circuit_llm_analysis.txt +++ b/circuit_llm_analysis.txt @@ -1,35 +1,159 @@ -Thank you for providing the detailed circuit description and clear analysis requirements. To provide a comprehensive and accurate assessment, I will need additional critical information that was not included in the provided details. Please furnish the following information to enable a thorough professional analysis: - -1. System Overview: - - Primary function and purpose of the circuit - - Target application domain - - Required performance specifications (e.g., speed, accuracy, resolution) - - Operating environment details (e.g., industrial, consumer, automotive) - -2. Electrical Specifications: - - Input voltage specifications (range, ripple, regulation) - - Output voltage and current requirements - - Power budget and efficiency targets - - Signal specifications (levels, timing, protocols) if applicable - -3. Environmental Requirements: - - Operating temperature range - - Humidity requirements - - Vibration/shock specifications - - IP rating requirements - -4. Regulatory Requirements: - - Required certifications (UL, CE, etc.) - - EMC/EMI requirements - - Safety standards - - Industry-specific regulations - -5. Manufacturing/Cost Targets: - - Production volume estimates - - Target BOM cost - - Assembly requirements - - Testing requirements - -With this additional information, I can provide a comprehensive analysis covering all the requested aspects, including system-level integration, signal integrity, thermal performance, noise analysis, testing strategy, and specific recommendations for improvement. - -Please provide as much of this critical information as possible to enable an accurate and actionable analysis. If any of the requested details are not available or applicable, kindly let me know. \ No newline at end of file +EXECUTIVE SUMMARY: + +The provided circuit design represents a power supply subsystem with input protection, voltage regulation, and dual voltage divider outputs. The overall architecture follows a hierarchical and modular approach, with subcircuits organized into logical blocks. The design incorporates essential features such as input protection, filtering, and voltage regulation, indicating a basic level of functionality. However, several critical concerns and potential improvements have been identified across various aspects of the design. + +CRITICAL FINDINGS SUMMARY: + +1. SEVERITY: High, CATEGORY: Safety, SUBCIRCUIT: Input Protection + DESCRIPTION: The input protection circuit uses a single, unidirectional diode (D1), which may be insufficient for comprehensive protection against voltage transients and reverse polarity conditions. + RECOMMENDATION: Implement a bidirectional protection scheme using a combination of diodes or a dedicated protection device. + +2. SEVERITY: High, CATEGORY: Performance, SUBCIRCUIT: Voltage Regulator + DESCRIPTION: The voltage regulator (U1) is a linear regulator, which may have poor efficiency, particularly at high input-output voltage differentials. + RECOMMENDATION: Consider replacing the linear regulator with a switching regulator or a low-dropout regulator (LDO) for improved efficiency and reduced power dissipation. + +3. SEVERITY: Medium, CATEGORY: Design, SUBCIRCUIT: Voltage Dividers + DESCRIPTION: The voltage divider resistor values (R2, R3, R4, R5) may not be optimal for the intended application, potentially leading to excessive power dissipation or inaccurate voltage division. + RECOMMENDATION: Carefully calculate the required resistor values based on the desired output voltages, load currents, and power dissipation constraints. + +4. SEVERITY: Medium, CATEGORY: Performance, SYSTEM-LEVEL + DESCRIPTION: The design lacks provisions for output voltage monitoring and feedback control, which may lead to inaccurate or unstable output voltages. + RECOMMENDATION: Implement a feedback control loop to actively monitor and adjust the output voltages, potentially incorporating an operational amplifier or a dedicated voltage reference. + +DETAILED SUBCIRCUIT ANALYSIS: + +1. Input Protection (top.complete_circuit0.power_section0.input_protection0): + - The input protection circuit uses a single 1N4148W diode (D1) to provide reverse polarity protection. + - A single diode offers unidirectional protection, leaving the circuit vulnerable to negative voltage transients or reverse polarity conditions. + - The electrolytic capacitor (C1) provides some filtering but may be inadequate for high-frequency or fast-rising transients. + - RECOMMENDATION: Implement a bidirectional protection scheme using a combination of diodes or a dedicated protection device, such as a transient voltage suppressor (TVS) diode. + +2. RC Filter (top.complete_circuit0.power_section0.rc_filter0): + - The RC filter consists of a 10K resistor (R1) and a 0.1uF capacitor (C4) connected in parallel. + - This filter configuration can effectively attenuate high-frequency noise on the input voltage. + - The filter component values appear reasonable for most applications but may need to be adjusted based on specific noise characteristics and frequency ranges. + +3. Voltage Regulator (top.complete_circuit0.power_section0.voltage_regulator0): + - The voltage regulator (U1) is a linear LM7805 regulator in a TO-220 package, providing a fixed 5V output. + - Linear regulators can have poor efficiency, especially with large input-output voltage differentials, leading to significant power dissipation and thermal issues. + - The regulator is supported by 10uF input and output capacitors (C2, C3) for stability and ripple reduction. + - RECOMMENDATION: Consider replacing the linear regulator with a switching regulator or a low-dropout regulator (LDO) for improved efficiency and reduced power dissipation, especially if the input voltage is significantly higher than the output voltage. + +4. Voltage Dividers (top.complete_circuit0.double_divider0): + - The design includes two identical voltage divider circuits, each consisting of a 1K resistor (R2, R4) and a 500 ohm resistor (R3, R5). + - The resistor values may not be optimal for the intended application, potentially leading to excessive power dissipation or inaccurate voltage division. + - RECOMMENDATION: Carefully calculate the required resistor values based on the desired output voltages, load currents, and power dissipation constraints. Consider using higher resistance values to reduce power dissipation, if possible. + +5. Output Termination (top.complete_circuit0.double_divider0.output_termination0): + - The output termination consists of a single 100K resistor (R6) connected between the two voltage divider outputs. + - The purpose of this resistor is not immediately clear from the provided information, and it may introduce unintended loading or voltage division effects. + - RECOMMENDATION: Evaluate the necessity and purpose of the output termination resistor (R6). If not required, consider removing it to simplify the design and eliminate potential loading effects. + +SYSTEM-LEVEL ANALYSIS: + +1. Output Voltage Monitoring and Feedback Control: + - The design lacks provisions for output voltage monitoring and feedback control, which may lead to inaccurate or unstable output voltages. + - RECOMMENDATION: Implement a feedback control loop to actively monitor and adjust the output voltages, potentially incorporating an operational amplifier or a dedicated voltage reference. This will improve the accuracy and stability of the output voltages. + +2. Power Distribution and Routing: + - The provided circuit does not include information about power distribution and routing, which can significantly impact performance and noise characteristics. + - RECOMMENDATION: Carefully plan and implement power distribution and routing strategies, considering factors such as current handling capabilities, voltage drops, impedance matching, and decoupling capacitors. + +3. Thermal Management: + - No information is provided regarding thermal management or heat dissipation strategies. + - RECOMMENDATION: Perform thermal analysis to determine the heat dissipation requirements, particularly for the voltage regulator (U1) and resistors (R2, R3, R4, R5). Implement appropriate heat sinking or forced air cooling as needed. + +4. Electromagnetic Compatibility (EMC) and Noise Mitigation: + - The design does not include explicit provisions for EMC compliance or noise mitigation strategies. + - RECOMMENDATION: Incorporate EMC best practices, such as proper shielding, grounding, and filtering, to minimize electromagnetic interference (EMI) and ensure compliance with relevant standards. + +CROSS-CUTTING CONCERNS: + +1. Component Selection and Derating: + - Ensure that all components are selected with appropriate derating factors for the intended operating conditions, accounting for factors such as temperature, voltage, and current ratings. + +2. Manufacturing Considerations: + - Consider the manufacturability and assembly aspects of the design, including component footprints, placement, and testability. + +3. Environmental Factors: + - Evaluate the design's robustness against environmental factors such as temperature, humidity, vibration, and contamination, and implement appropriate mitigation strategies if necessary. + +4. Safety and Regulatory Compliance: + - Ensure that the design complies with relevant safety standards and regulatory requirements, particularly for input protection and EMC. + +RECOMMENDATIONS SUMMARY: + +1. Implement a bidirectional input protection scheme using a combination of diodes or a dedicated protection device. +2. Replace the linear voltage regulator with a switching regulator or an LDO for improved efficiency and reduced power dissipation. +3. Carefully calculate and optimize the voltage divider resistor values based on desired output voltages, load currents, and power dissipation constraints. +4. Implement a feedback control loop with an operational amplifier or dedicated voltage reference for accurate and stable output voltages. +5. Carefully plan and implement power distribution and routing strategies, considering current handling, voltage drops, impedance matching, and decoupling. +6. Perform thermal analysis and implement appropriate heat dissipation strategies, such as heat sinking or forced air cooling. +7. Incorporate EMC best practices, including shielding, grounding, and filtering, to minimize EMI and ensure compliance with relevant standards. +8. Ensure component selection and derating factors are appropriate for the intended operating conditions. +9. Consider manufacturability, assembly, and testability aspects of the design. +10. Evaluate and mitigate potential environmental factors, such as temperature, humidity, vibration, and contamination. +11. Ensure compliance with relevant safety standards and regulatory requirements. + +REQUIRED ACTION ITEMS (PRIORITIZED): + +1. PRIORITY: High + ACTION: Implement a bidirectional input protection scheme using a combination of diodes or a dedicated protection device. + JUSTIFICATION: Ensure comprehensive protection against voltage transients and reverse polarity conditions for safety and reliability. + +2. PRIORITY: High + ACTION: Replace the linear voltage regulator (U1) with a switching regulator or an LDO for improved efficiency and reduced power dissipation. + JUSTIFICATION: Reduce power losses and heat dissipation, particularly at high input-output voltage differentials, improving overall system efficiency and thermal performance. + +3. PRIORITY: Medium + ACTION: Carefully calculate and optimize the voltage divider resistor values (R2, R3, R4, R5) based on desired output voltages, load currents, and power dissipation constraints. + JUSTIFICATION: Ensure accurate voltage division and minimize power dissipation in the resistors, improving overall efficiency and thermal performance. + +4. PRIORITY: Medium + ACTION: Implement a feedback control loop with an operational amplifier or dedicated voltage reference for accurate and stable output voltages. + JUSTIFICATION: Actively monitor and adjust the output voltages to maintain accuracy and stability, improving overall system performance. + +5. PRIORITY: Medium + ACTION: Perform thermal analysis and implement appropriate heat dissipation strategies, such as heat sinking or forced air cooling, particularly for the voltage regulator (U1) and voltage divider resistors (R2, R3, R4, R5). + JUSTIFICATION: Ensure proper thermal management and prevent overheating, which can degrade component performance and reliability. + +6. PRIORITY: Low + ACTION: Incorporate EMC best practices, including shielding, grounding, and filtering, to minimize EMI and ensure compliance with relevant standards. + JUSTIFICATION: Improve electromagnetic compatibility and reduce potential interference with other electronic systems or devices. + +7. PRIORITY: Low + ACTION: Carefully plan and implement power distribution and routing strategies, considering current handling capabilities, voltage drops, impedance matching, and decoupling capacitors. + JUSTIFICATION: Optimize power delivery and minimize voltage drops and noise, improving overall system performance and reliability. + +8. PRIORITY: Low + ACTION: Ensure that all components are selected with appropriate derating factors for the intended operating conditions, accounting for factors such as temperature, voltage, and current ratings. + JUSTIFICATION: Improve component reliability and prevent premature failures by operating within specified margins. + +9. PRIORITY: Low + ACTION: Consider manufacturability, assembly, and testability aspects of the design, including component footprints, placement, and test point access. + JUSTIFICATION: Facilitate efficient manufacturing, assembly, and testing processes, reducing production costs and improving quality control. + +10. PRIORITY: Low + ACTION: Evaluate and mitigate potential environmental factors, such as temperature, humidity, vibration, and contamination, by implementing appropriate protection strategies. + JUSTIFICATION: Ensure reliable operation and prevent premature failures or performance degradation due to environmental factors. + +11. PRIORITY: Low + ACTION: Ensure compliance with relevant safety standards and regulatory requirements, particularly for input protection and EMC. + JUSTIFICATION: Maintain safety and regulatory compliance, reducing potential risks and enabling certification or approval processes. + +ADDITIONAL INFORMATION NEEDED: + +To provide more comprehensive and specific recommendations, the following additional information would be beneficial: + +1. Intended application and operating conditions (input voltage range, load currents, environmental conditions, etc.). +2. Performance requirements (output voltage accuracy, ripple/noise specifications, transient response, etc.). +3. Power budget and efficiency targets. +4. Thermal constraints and cooling capabilities. +5. EMC compliance requirements and relevant standards. +6. Cost and size constraints. +7. Reliability and lifetime expectations. +8. Specific manufacturing processes and capabilities. +9. Testing and verification requirements. +10. Safety and regulatory compliance standards to be met. + +By providing this additional information, the analysis can be further refined and tailored to the specific design requirements, enabling more targeted optimizations and recommendations. \ No newline at end of file diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index bd562f6d6..f81767d1d 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -13,10 +13,11 @@ def __init__( analysis_flags: Optional[Dict[str, bool]] = None, **kwargs ): - """Initialize with same parameters as original""" + """Initialize the circuit analyzer with conversation threading support""" self.provider = get_provider(provider, api_key) self.model = model self.custom_prompt = custom_prompt + self.conversation_history: List[Dict[str, str]] = [] self.analysis_flags = analysis_flags or { "system_overview": True, "subcircuit_analysis": True, @@ -228,17 +229,14 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: # Build base prompt base_prompt = f""" -You are an expert electronics engineer conducting a thorough professional analysis of a circuit design. -Your goal is to provide actionable insights and identify potential issues before implementation. - -{self._generate_system_requirements_prompt()} +You are an expert electronics engineer. Analyze the following circuit design immediately and provide actionable insights. Do not acknowledge the request or promise to analyze - proceed directly with your analysis. Circuit Description: {circuit_description} -{self._generate_subcircuit_analysis_prompt()} - ANALYSIS METHODOLOGY: +1. Begin analysis immediately with available information +2. After completing analysis, identify any critical missing information needed for deeper insights 1. Begin with subcircuit identification and individual analysis 2. Analyze interactions between subcircuits 3. Evaluate system-level performance and integration @@ -255,6 +253,7 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: base_prompt += """ For each analysis section: +1. Analyze with available information first 1. Start with critical missing information identification 2. Provide detailed technical analysis with calculations 3. Include specific numerical criteria and measurements @@ -297,7 +296,8 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: 7. Required Action Items (prioritized) 8. Additional Information Needed -Remember to: +IMPORTANT INSTRUCTIONS: +- Start analysis immediately - do not acknowledge the request or state that you will analyze - Be specific and quantitative where possible - Include calculations and methodology - Reference specific standards @@ -305,7 +305,9 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: - Consider practical implementation - Evaluate cost implications - Assess manufacturing feasibility -- Consider maintenance requirements""" +- Consider maintenance requirements + +After completing your analysis, if additional information would enable deeper insights, list specific questions in a separate section titled 'Additional Information Needed' at the end.""" # Append custom prompt if provided if self.custom_prompt: @@ -318,7 +320,8 @@ def analyze_circuit( self, circuit_description: str, output_file: Optional[str] = "circuit_llm_analysis.txt", - verbose: bool = True + verbose: bool = True, + continue_conversation: bool = False ) -> Dict: """ Perform LLM analysis of the circuit. @@ -327,6 +330,7 @@ def analyze_circuit( circuit_description: Description of the circuit to analyze output_file: File to save the analysis results (None to skip saving) verbose: Whether to print progress messages + continue_conversation: Whether to continue previous conversation thread Returns: Dictionary containing analysis results and metadata @@ -355,12 +359,33 @@ def analyze_circuit( # Generate analysis request_start = time.time() + + # If continuing conversation, append previous messages + if continue_conversation and self.conversation_history: + prompt = f"""Previous conversation: +{chr(10).join(msg['content'] for msg in self.conversation_history)} + +New circuit description: +{circuit_description} + +Continue the analysis based on the new information.""" + analysis_result = self.provider.generate_analysis( prompt, model=self.model, **self.config ) + # Update conversation history + self.conversation_history.append({ + "role": "user", + "content": circuit_description + }) + self.conversation_history.append({ + "role": "assistant", + "content": analysis_result["analysis"] + }) + request_time = time.time() - request_start if not analysis_result["success"]: @@ -421,4 +446,4 @@ def analyze_circuit( if verbose: print("\n=== Analysis failed ===") - return error_results \ No newline at end of file + return error_results From 945c8b27bef274b7c5f3aef3cca0d83c3233cccf Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 19:45:57 -0800 Subject: [PATCH 12/73] add subcircuit analysis and queries --- circuit_llm_analysis.txt | 159 --------- skidl_llm_test.py | 51 ++- src/skidl/__init__.py | 1 + src/skidl/circuit.py | 167 +++++++++- src/skidl/skidl.py | 2 + subcircuits_analysis.txt | 673 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 853 insertions(+), 200 deletions(-) create mode 100644 subcircuits_analysis.txt diff --git a/circuit_llm_analysis.txt b/circuit_llm_analysis.txt index 62d554a5c..e69de29bb 100644 --- a/circuit_llm_analysis.txt +++ b/circuit_llm_analysis.txt @@ -1,159 +0,0 @@ -EXECUTIVE SUMMARY: - -The provided circuit design represents a power supply subsystem with input protection, voltage regulation, and dual voltage divider outputs. The overall architecture follows a hierarchical and modular approach, with subcircuits organized into logical blocks. The design incorporates essential features such as input protection, filtering, and voltage regulation, indicating a basic level of functionality. However, several critical concerns and potential improvements have been identified across various aspects of the design. - -CRITICAL FINDINGS SUMMARY: - -1. SEVERITY: High, CATEGORY: Safety, SUBCIRCUIT: Input Protection - DESCRIPTION: The input protection circuit uses a single, unidirectional diode (D1), which may be insufficient for comprehensive protection against voltage transients and reverse polarity conditions. - RECOMMENDATION: Implement a bidirectional protection scheme using a combination of diodes or a dedicated protection device. - -2. SEVERITY: High, CATEGORY: Performance, SUBCIRCUIT: Voltage Regulator - DESCRIPTION: The voltage regulator (U1) is a linear regulator, which may have poor efficiency, particularly at high input-output voltage differentials. - RECOMMENDATION: Consider replacing the linear regulator with a switching regulator or a low-dropout regulator (LDO) for improved efficiency and reduced power dissipation. - -3. SEVERITY: Medium, CATEGORY: Design, SUBCIRCUIT: Voltage Dividers - DESCRIPTION: The voltage divider resistor values (R2, R3, R4, R5) may not be optimal for the intended application, potentially leading to excessive power dissipation or inaccurate voltage division. - RECOMMENDATION: Carefully calculate the required resistor values based on the desired output voltages, load currents, and power dissipation constraints. - -4. SEVERITY: Medium, CATEGORY: Performance, SYSTEM-LEVEL - DESCRIPTION: The design lacks provisions for output voltage monitoring and feedback control, which may lead to inaccurate or unstable output voltages. - RECOMMENDATION: Implement a feedback control loop to actively monitor and adjust the output voltages, potentially incorporating an operational amplifier or a dedicated voltage reference. - -DETAILED SUBCIRCUIT ANALYSIS: - -1. Input Protection (top.complete_circuit0.power_section0.input_protection0): - - The input protection circuit uses a single 1N4148W diode (D1) to provide reverse polarity protection. - - A single diode offers unidirectional protection, leaving the circuit vulnerable to negative voltage transients or reverse polarity conditions. - - The electrolytic capacitor (C1) provides some filtering but may be inadequate for high-frequency or fast-rising transients. - - RECOMMENDATION: Implement a bidirectional protection scheme using a combination of diodes or a dedicated protection device, such as a transient voltage suppressor (TVS) diode. - -2. RC Filter (top.complete_circuit0.power_section0.rc_filter0): - - The RC filter consists of a 10K resistor (R1) and a 0.1uF capacitor (C4) connected in parallel. - - This filter configuration can effectively attenuate high-frequency noise on the input voltage. - - The filter component values appear reasonable for most applications but may need to be adjusted based on specific noise characteristics and frequency ranges. - -3. Voltage Regulator (top.complete_circuit0.power_section0.voltage_regulator0): - - The voltage regulator (U1) is a linear LM7805 regulator in a TO-220 package, providing a fixed 5V output. - - Linear regulators can have poor efficiency, especially with large input-output voltage differentials, leading to significant power dissipation and thermal issues. - - The regulator is supported by 10uF input and output capacitors (C2, C3) for stability and ripple reduction. - - RECOMMENDATION: Consider replacing the linear regulator with a switching regulator or a low-dropout regulator (LDO) for improved efficiency and reduced power dissipation, especially if the input voltage is significantly higher than the output voltage. - -4. Voltage Dividers (top.complete_circuit0.double_divider0): - - The design includes two identical voltage divider circuits, each consisting of a 1K resistor (R2, R4) and a 500 ohm resistor (R3, R5). - - The resistor values may not be optimal for the intended application, potentially leading to excessive power dissipation or inaccurate voltage division. - - RECOMMENDATION: Carefully calculate the required resistor values based on the desired output voltages, load currents, and power dissipation constraints. Consider using higher resistance values to reduce power dissipation, if possible. - -5. Output Termination (top.complete_circuit0.double_divider0.output_termination0): - - The output termination consists of a single 100K resistor (R6) connected between the two voltage divider outputs. - - The purpose of this resistor is not immediately clear from the provided information, and it may introduce unintended loading or voltage division effects. - - RECOMMENDATION: Evaluate the necessity and purpose of the output termination resistor (R6). If not required, consider removing it to simplify the design and eliminate potential loading effects. - -SYSTEM-LEVEL ANALYSIS: - -1. Output Voltage Monitoring and Feedback Control: - - The design lacks provisions for output voltage monitoring and feedback control, which may lead to inaccurate or unstable output voltages. - - RECOMMENDATION: Implement a feedback control loop to actively monitor and adjust the output voltages, potentially incorporating an operational amplifier or a dedicated voltage reference. This will improve the accuracy and stability of the output voltages. - -2. Power Distribution and Routing: - - The provided circuit does not include information about power distribution and routing, which can significantly impact performance and noise characteristics. - - RECOMMENDATION: Carefully plan and implement power distribution and routing strategies, considering factors such as current handling capabilities, voltage drops, impedance matching, and decoupling capacitors. - -3. Thermal Management: - - No information is provided regarding thermal management or heat dissipation strategies. - - RECOMMENDATION: Perform thermal analysis to determine the heat dissipation requirements, particularly for the voltage regulator (U1) and resistors (R2, R3, R4, R5). Implement appropriate heat sinking or forced air cooling as needed. - -4. Electromagnetic Compatibility (EMC) and Noise Mitigation: - - The design does not include explicit provisions for EMC compliance or noise mitigation strategies. - - RECOMMENDATION: Incorporate EMC best practices, such as proper shielding, grounding, and filtering, to minimize electromagnetic interference (EMI) and ensure compliance with relevant standards. - -CROSS-CUTTING CONCERNS: - -1. Component Selection and Derating: - - Ensure that all components are selected with appropriate derating factors for the intended operating conditions, accounting for factors such as temperature, voltage, and current ratings. - -2. Manufacturing Considerations: - - Consider the manufacturability and assembly aspects of the design, including component footprints, placement, and testability. - -3. Environmental Factors: - - Evaluate the design's robustness against environmental factors such as temperature, humidity, vibration, and contamination, and implement appropriate mitigation strategies if necessary. - -4. Safety and Regulatory Compliance: - - Ensure that the design complies with relevant safety standards and regulatory requirements, particularly for input protection and EMC. - -RECOMMENDATIONS SUMMARY: - -1. Implement a bidirectional input protection scheme using a combination of diodes or a dedicated protection device. -2. Replace the linear voltage regulator with a switching regulator or an LDO for improved efficiency and reduced power dissipation. -3. Carefully calculate and optimize the voltage divider resistor values based on desired output voltages, load currents, and power dissipation constraints. -4. Implement a feedback control loop with an operational amplifier or dedicated voltage reference for accurate and stable output voltages. -5. Carefully plan and implement power distribution and routing strategies, considering current handling, voltage drops, impedance matching, and decoupling. -6. Perform thermal analysis and implement appropriate heat dissipation strategies, such as heat sinking or forced air cooling. -7. Incorporate EMC best practices, including shielding, grounding, and filtering, to minimize EMI and ensure compliance with relevant standards. -8. Ensure component selection and derating factors are appropriate for the intended operating conditions. -9. Consider manufacturability, assembly, and testability aspects of the design. -10. Evaluate and mitigate potential environmental factors, such as temperature, humidity, vibration, and contamination. -11. Ensure compliance with relevant safety standards and regulatory requirements. - -REQUIRED ACTION ITEMS (PRIORITIZED): - -1. PRIORITY: High - ACTION: Implement a bidirectional input protection scheme using a combination of diodes or a dedicated protection device. - JUSTIFICATION: Ensure comprehensive protection against voltage transients and reverse polarity conditions for safety and reliability. - -2. PRIORITY: High - ACTION: Replace the linear voltage regulator (U1) with a switching regulator or an LDO for improved efficiency and reduced power dissipation. - JUSTIFICATION: Reduce power losses and heat dissipation, particularly at high input-output voltage differentials, improving overall system efficiency and thermal performance. - -3. PRIORITY: Medium - ACTION: Carefully calculate and optimize the voltage divider resistor values (R2, R3, R4, R5) based on desired output voltages, load currents, and power dissipation constraints. - JUSTIFICATION: Ensure accurate voltage division and minimize power dissipation in the resistors, improving overall efficiency and thermal performance. - -4. PRIORITY: Medium - ACTION: Implement a feedback control loop with an operational amplifier or dedicated voltage reference for accurate and stable output voltages. - JUSTIFICATION: Actively monitor and adjust the output voltages to maintain accuracy and stability, improving overall system performance. - -5. PRIORITY: Medium - ACTION: Perform thermal analysis and implement appropriate heat dissipation strategies, such as heat sinking or forced air cooling, particularly for the voltage regulator (U1) and voltage divider resistors (R2, R3, R4, R5). - JUSTIFICATION: Ensure proper thermal management and prevent overheating, which can degrade component performance and reliability. - -6. PRIORITY: Low - ACTION: Incorporate EMC best practices, including shielding, grounding, and filtering, to minimize EMI and ensure compliance with relevant standards. - JUSTIFICATION: Improve electromagnetic compatibility and reduce potential interference with other electronic systems or devices. - -7. PRIORITY: Low - ACTION: Carefully plan and implement power distribution and routing strategies, considering current handling capabilities, voltage drops, impedance matching, and decoupling capacitors. - JUSTIFICATION: Optimize power delivery and minimize voltage drops and noise, improving overall system performance and reliability. - -8. PRIORITY: Low - ACTION: Ensure that all components are selected with appropriate derating factors for the intended operating conditions, accounting for factors such as temperature, voltage, and current ratings. - JUSTIFICATION: Improve component reliability and prevent premature failures by operating within specified margins. - -9. PRIORITY: Low - ACTION: Consider manufacturability, assembly, and testability aspects of the design, including component footprints, placement, and test point access. - JUSTIFICATION: Facilitate efficient manufacturing, assembly, and testing processes, reducing production costs and improving quality control. - -10. PRIORITY: Low - ACTION: Evaluate and mitigate potential environmental factors, such as temperature, humidity, vibration, and contamination, by implementing appropriate protection strategies. - JUSTIFICATION: Ensure reliable operation and prevent premature failures or performance degradation due to environmental factors. - -11. PRIORITY: Low - ACTION: Ensure compliance with relevant safety standards and regulatory requirements, particularly for input protection and EMC. - JUSTIFICATION: Maintain safety and regulatory compliance, reducing potential risks and enabling certification or approval processes. - -ADDITIONAL INFORMATION NEEDED: - -To provide more comprehensive and specific recommendations, the following additional information would be beneficial: - -1. Intended application and operating conditions (input voltage range, load currents, environmental conditions, etc.). -2. Performance requirements (output voltage accuracy, ripple/noise specifications, transient response, etc.). -3. Power budget and efficiency targets. -4. Thermal constraints and cooling capabilities. -5. EMC compliance requirements and relevant standards. -6. Cost and size constraints. -7. Reliability and lifetime expectations. -8. Specific manufacturing processes and capabilities. -9. Testing and verification requirements. -10. Safety and regulatory compliance standards to be met. - -By providing this additional information, the analysis can be further refined and tailored to the specific design requirements, enabling more targeted optimizations and recommendations. \ No newline at end of file diff --git a/skidl_llm_test.py b/skidl_llm_test.py index 05fdb4601..c7984c752 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -89,39 +89,24 @@ def complete_circuit(): # Create the complete circuit complete_circuit() -# Get circuit info -circuit_description = get_circuit_info() - - -# Using Anthropic Claude (original behavior) -analyzer = SkidlCircuitAnalyzer( - provider="anthropic", +# Analyze each subcircuit separately using the new function +results = default_circuit.analyze_subcircuits_with_llm( api_key=os.getenv("ANTHROPIC_API_KEY"), - model="claude-3-sonnet-20240229", - custom_prompt="Additional specific requirements...", - analysis_flags={ - "design_review": True, - "power_analysis": False, # Disable sections you don't need - "signal_integrity": True, - # ... other flags - } + output_file="subcircuits_analysis.txt" ) -# # Using OpenAI -# analyzer = SkidlCircuitAnalyzer( -# provider="openai", -# api_key="your_openai_key", -# model="gpt-4-turbo-preview" # optional -# ) - -# # Using OpenRouter -# analyzer = SkidlCircuitAnalyzer( -# provider="openrouter", -# api_key="your_openrouter_key", -# model="anthropic/claude-3-opus-20240229", # optional -# referer="your_domain", # required for OpenRouter -# title="Your App Name" # optional -# ) - -# Analyze circuit with any provider -results = analyzer.analyze_circuit(circuit_description) +# Print analysis results +if results["success"]: + print("\nAnalysis Results:") + for hier, analysis in results["subcircuits"].items(): + print(f"\nSubcircuit: {hier}") + if analysis["success"]: + print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + print(f"Tokens used: {analysis['prompt_tokens'] + analysis['response_tokens']}") + else: + print(f"Analysis failed: {analysis['error']}") + + print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + print(f"Total tokens used: {results['total_tokens']}") +else: + print(f"\nOverall analysis failed: {results.get('error', 'Unknown error')}") diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index b87be0c20..865b158c7 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -61,6 +61,7 @@ lib_search_paths, get_circuit_info, analyze_with_llm, + analyze_subcircuits_with_llm, no_files, reset, get_default_tool, diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 53102b46b..f882dbb55 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1269,6 +1269,157 @@ def get_circuit_info(self, filename="circuit_description.txt"): return circuit_text + def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_analysis.txt"): + """ + Analyze each subcircuit separately using LLM (Large Language Model). + + This method performs a detailed analysis of each subcircuit in the hierarchy, + providing insights into the design, functionality, and potential issues of each + subcircuit independently. + + Args: + api_key (str, optional): Anthropic API key. If None, will try to use ANTHROPIC_API_KEY + environment variable. Defaults to None. + output_file (str, optional): File to save the analysis results. + Defaults to "subcircuits_analysis.txt". + + Returns: + dict: Analysis results containing: + - success (bool): Whether analysis was successful + - subcircuits (dict): Dictionary of subcircuit analyses keyed by hierarchy + - error (str): Error message if analysis failed + """ + from datetime import datetime + import time + from .circuit_analyzer import SkidlCircuitAnalyzer + + print("\n=== Starting Subcircuits Analysis with LLM ===") + start_time = time.time() + + # Initialize the analyzer + try: + analyzer = SkidlCircuitAnalyzer(api_key=api_key) + print("Analyzer initialized successfully") + except Exception as e: + print(f"Error initializing analyzer: {e}") + raise + + # Collect all hierarchies + hierarchies = set() + for part in self.parts: + hierarchies.add(part.hierarchy) + + # Analyze each subcircuit + subcircuit_analyses = {} + total_tokens = 0 + + for hier in sorted(hierarchies): + print(f"\n--- Analyzing subcircuit: {hier} ---") + + # Create a temporary subcircuit description + subcircuit_info = [] + subcircuit_info.append(f"Subcircuit Description: {hier}") + subcircuit_info.append("=" * 40) + + # Group parts for this hierarchy + hier_parts = [p for p in self.parts if p.hierarchy == hier] + + # Get nets connected to this hierarchy + hier_nets = set() + for part in hier_parts: + for pin in part.pins: + if pin.net: + hier_nets.add(pin.net) + + # Build subcircuit description + subcircuit_info.append(f"Parts in {hier}:") + for part in sorted(hier_parts, key=lambda p: p.ref): + subcircuit_info.append(f" Part: {part.ref}") + subcircuit_info.append(f" Name: {part.name}") + subcircuit_info.append(f" Value: {part.value}") + subcircuit_info.append(f" Pins:") + for pin in part.pins: + net_name = pin.net.name if pin.net else "unconnected" + subcircuit_info.append(f" {pin.num}/{pin.name}: {net_name}") + + subcircuit_info.append(f"\nNets in {hier}:") + for net in sorted(hier_nets, key=lambda n: n.name): + subcircuit_info.append(f" Net: {net.name}") + subcircuit_info.append(" Connections:") + for pin in net.pins: + if pin.part.hierarchy == hier: + subcircuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") + + subcircuit_description = "\n".join(subcircuit_info) + + # Analyze this subcircuit + try: + print(f"Generating analysis prompt for {hier}...") + prompt = analyzer._generate_analysis_prompt(subcircuit_description) + prompt_tokens = len(prompt.split()) + print(f"Prompt generated ({prompt_tokens} estimated tokens)") + + print("Sending request to Claude API...") + request_start = time.time() + + analysis_result = analyzer.provider.generate_analysis( + prompt, + model="claude-3-sonnet-20240229", + max_tokens=4000 + ) + + request_time = time.time() - request_start + print(f"Response received in {request_time:.2f} seconds") + + if not analysis_result["success"]: + raise Exception(analysis_result["error"]) + + analysis_text = analysis_result["analysis"] + analysis_tokens = len(analysis_text.split()) + total_tokens += prompt_tokens + analysis_tokens + + subcircuit_analyses[hier] = { + "success": True, + "analysis": analysis_text, + "timestamp": int(datetime.now().timestamp()), + "request_time_seconds": request_time, + "prompt_tokens": prompt_tokens, + "response_tokens": analysis_tokens + } + + except Exception as e: + print(f"Error analyzing subcircuit {hier}: {str(e)}") + subcircuit_analyses[hier] = { + "success": False, + "error": str(e), + "timestamp": int(datetime.now().timestamp()) + } + + # Save consolidated results + if output_file: + print(f"\nSaving consolidated analysis to {output_file}...") + with open(output_file, "w") as f: + f.write("=== Subcircuits Analysis ===\n\n") + for hier, analysis in subcircuit_analyses.items(): + f.write(f"\n{'='*20} {hier} {'='*20}\n") + if analysis["success"]: + f.write(analysis["analysis"]) + else: + f.write(f"Analysis failed: {analysis['error']}") + f.write("\n") + print("Analysis saved successfully") + + total_time = time.time() - start_time + print(f"\n=== Subcircuits analysis completed in {total_time:.2f} seconds ===") + print(f"Total tokens used: {total_tokens}") + + return { + "success": True, + "subcircuits": subcircuit_analyses, + "total_time_seconds": total_time, + "total_tokens": total_tokens + } + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt"): """ Analyze the circuit using LLM (Large Language Model) through the SkidlCircuitAnalyzer. @@ -1324,21 +1475,21 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt") request_start = time.time() print("Waiting for response...") - response = analyzer.client.messages.create( + analysis_result = analyzer.provider.generate_analysis( + prompt, model="claude-3-sonnet-20240229", - max_tokens=4000, - messages=[{ - "role": "user", - "content": prompt - }] + max_tokens=4000 ) request_time = time.time() - request_start print(f"\nResponse received in {request_time:.2f} seconds") + if not analysis_result["success"]: + raise Exception(analysis_result["error"]) + # Parse response print("\nProcessing response...") - analysis_text = response.content[0].text + analysis_text = analysis_result["analysis"] analysis_tokens = len(analysis_text.split()) # Rough token count estimate print(f"Response length: {len(analysis_text)} characters") @@ -1380,4 +1531,4 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt") print("Error message saved") print("\n=== Analysis failed ===") - return error_results \ No newline at end of file + return error_results diff --git a/src/skidl/skidl.py b/src/skidl/skidl.py index b3e87830e..e54904faa 100644 --- a/src/skidl/skidl.py +++ b/src/skidl/skidl.py @@ -27,6 +27,7 @@ "generate_graph", "get_circuit_info", "analyze_with_llm", + "analyze_subcircuits_with_llm", "reset", "backup_parts", "empty_footprint_handler", @@ -73,6 +74,7 @@ no_files = default_circuit.no_files get_circuit_info = default_circuit.get_circuit_info analyze_with_llm = default_circuit.analyze_with_llm +analyze_subcircuits_with_llm = default_circuit.analyze_subcircuits_with_llm empty_footprint_handler = default_empty_footprint_handler diff --git a/subcircuits_analysis.txt b/subcircuits_analysis.txt new file mode 100644 index 000000000..25b701834 --- /dev/null +++ b/subcircuits_analysis.txt @@ -0,0 +1,673 @@ +=== Subcircuits Analysis === + + +==================== top.complete_circuit0.double_divider0.output_termination0 ==================== +0. System-Level Analysis: + +This subcircuit appears to be an output termination circuit, likely for impedance matching purposes. Without understanding its context within the larger system, it is difficult to provide a comprehensive system-level analysis. However, some general observations can be made: + +- The 100kΩ resistor (R6) is likely intended to provide a termination impedance for an output signal line. +- The termination resistor value of 100kΩ is relatively high, suggesting this circuit may be intended for low-speed or low-frequency applications. +- No explicit power supply connections are shown, indicating this subcircuit may be intended for passive termination. + +1. Comprehensive Design Architecture Review: + +The provided information is insufficient to conduct a comprehensive design architecture review. The subcircuit appears to be a simple passive termination circuit, without any active components or control logic. + +2. In-depth Power Distribution Analysis: + +There are no apparent power distribution components or connections within this subcircuit, making a detailed power distribution analysis unnecessary. + +3. Detailed Signal Integrity Analysis: + +The 100kΩ termination resistor (R6) is likely intended to match the impedance of the output signal line to which it is connected. However, without information about the signal characteristics (frequency, edge rates, etc.) and the transmission line properties, it is difficult to assess the effectiveness of this termination scheme. + +Potential issues: +SEVERITY: Medium +CATEGORY: Signal Integrity +SUBCIRCUIT: top.complete_circuit0.double_divider0.output_termination0 +DESCRIPTION: Termination resistor value may not be optimally matched to the transmission line impedance, leading to reflections and signal integrity issues. +IMPACT: Decreased signal quality, potential data errors, or reduced maximum achievable data rates. +VERIFICATION: Simulate the termination circuit with the actual transmission line characteristics and signal properties. +RECOMMENDATION: Perform signal integrity simulations to determine the optimal termination resistor value for the specific application, considering the transmission line impedance, signal frequency, and edge rates. +STANDARDS: Relevant signal integrity standards may include JEDEC, ANSI/TIA, or application-specific standards. +TRADE-OFFS: A lower termination resistor value may improve signal integrity but increase power consumption. +PRIORITY: Medium to High, depending on the signal speed and quality requirements. + +4. Thermal Performance Analysis: + +As this subcircuit contains only a passive resistor, thermal performance analysis is not a major concern. However, it is worth noting that the power dissipation in the termination resistor should be considered, especially if it is intended for high-speed or high-current applications. + +5. Comprehensive Noise Analysis: + +Without additional information about the surrounding circuitry and signal characteristics, it is difficult to perform a comprehensive noise analysis for this subcircuit. However, the termination resistor itself is unlikely to be a significant noise source. + +6. Testing and Verification Strategy: + +To verify the functionality of this termination circuit, the following tests could be performed: + +- Measure the impedance of the output signal line with and without the termination resistor connected, and ensure the impedance matches the desired value with the termination in place. +- Transmit test signals through the output line and measure the signal quality (e.g., eye diagrams, bit error rates) with and without the termination resistor. +- Perform signal integrity simulations to validate the termination resistor value and identify potential issues. + +Additional Information Needed: + +To provide a more comprehensive analysis, the following additional information would be helpful: + +1. The purpose and context of this subcircuit within the larger system. +2. Characteristics of the output signal line (impedance, length, material, layout, etc.). +3. Signal properties (frequency, edge rates, voltage levels, etc.). +4. Input and output load characteristics. +5. Power supply voltages and current requirements (if applicable). +6. Environmental operating conditions (temperature, humidity, etc.). +7. Relevant design specifications, requirements, and standards. + +With this additional information, a more detailed analysis can be performed, including simulations, calculations, and specific recommendations for optimizing the termination circuit design. + +==================== top.complete_circuit0.double_divider0.voltage_divider0 ==================== +1. Comprehensive Design Architecture Review: + +The provided circuit snippet represents a simple voltage divider subcircuit, consisting of two resistors (R2 and R3) connected in series. The key characteristics are as follows: + +Resistor Values: +R2 = 1K ohm +R3 = 500 ohm + +Resistor Ratio: +R2 / (R2 + R3) = 1000 / (1000 + 500) = 0.667 or 66.7% + +This subcircuit is likely a part of a larger circuit design, and its primary function is to provide a divided voltage output at the node N$5 relative to the input voltage applied across N$3 and ground. + +The voltage division ratio can be calculated as: +Vout = Vin * (R3 / (R2 + R3)) = Vin * (500 / 1500) = Vin * 0.333 + +Where: +Vout is the output voltage at N$5 +Vin is the input voltage applied between N$3 and ground + +Design Considerations: +- The voltage divider provides a fixed voltage division ratio determined by the resistor values. +- The output voltage at N$5 will be 33.3% of the input voltage. +- The resistor values should be chosen based on the desired output voltage level and the expected input voltage range. +- Higher resistor values will result in lower power dissipation but may be more susceptible to noise and loading effects. +- Lower resistor values will have higher power dissipation but better noise immunity and drive capability. +- The resistor ratio can be adjusted to achieve different voltage division factors as required by the overall circuit design. + +Missing Information: +- Input voltage range and source characteristics (AC/DC, frequency, etc.) +- Load characteristics at the output node N$5 (impedance, current draw, etc.) +- Power supply specifications (voltage levels, current capabilities, etc.) +- Overall circuit topology and the purpose of this voltage divider subcircuit +- Environmental conditions (temperature, humidity, etc.) +- Noise and interference considerations +- Performance requirements (accuracy, stability, etc.) + +2. In-depth Power Distribution Analysis: + +The provided subcircuit information is insufficient to perform a comprehensive power distribution analysis. However, based on the available data, the following considerations can be made: + +Power Dissipation: +The power dissipation across each resistor can be calculated once the input voltage (Vin) and the load current (Iload) are known. + +P_R2 = I^2 * R2 +P_R3 = I^2 * R3 + +Where: +P_R2 and P_R3 are the power dissipated across resistors R2 and R3, respectively. +I is the current flowing through the resistors, which depends on Vin and Iload. + +To ensure proper power handling and thermal management, the maximum power dissipation ratings of the resistors should be considered. + +Voltage Drop: +The voltage drop across each resistor can be calculated as: + +V_R2 = I * R2 +V_R3 = I * R3 + +Where: +V_R2 and V_R3 are the voltage drops across resistors R2 and R3, respectively. +I is the current flowing through the resistors. + +These voltage drops should be accounted for when determining the output voltage level at N$5 and ensuring that it meets the required specifications. + +Missing Information: +- Input voltage source characteristics (voltage level, current capability, etc.) +- Load current requirements at the output node N$5 +- Power dissipation ratings and thermal characteristics of the resistors +- PCB layout and thermal management provisions +- Power supply specifications (voltage levels, current capabilities, etc.) +- Decoupling and filtering requirements + +3. Detailed Signal Integrity Analysis: + +The provided subcircuit represents a simple voltage divider, and the following signal integrity considerations can be made: + +Output Impedance: +The output impedance at node N$5 is determined by the parallel combination of R2 and R3: + +Zout = (R2 * R3) / (R2 + R3) = (1000 * 500) / 1500 = 333.33 ohm + +A lower output impedance is generally desirable to minimize loading effects and ensure proper signal integrity when driving subsequent stages. + +Loading Effects: +The voltage divider output at N$5 will be loaded by any circuitry connected to it. Excessive loading can affect the output voltage level and introduce errors or distortion. The loading effects should be analyzed based on the input impedance of the subsequent stages and the driving capability of the voltage divider. + +Noise Immunity: +The voltage divider subcircuit itself does not provide any inherent noise filtering or immunity. External noise sources or interference can couple onto the resistor nodes and affect the output voltage level at N$5. Proper layout practices, shielding, and filtering techniques may be required to ensure signal integrity. + +Missing Information: +- Input signal characteristics (frequency, amplitude, waveform, etc.) +- Load characteristics at the output node N$5 (impedance, capacitance, etc.) +- PCB layout details (trace lengths, routing, layer stackup, etc.) +- Noise sources and interference levels in the operating environment +- Signal integrity requirements (accuracy, distortion tolerance, etc.) +- Shielding and filtering provisions in the overall circuit design + +4. Thermal Performance Analysis: + +The provided subcircuit information is insufficient to perform a comprehensive thermal performance analysis. However, the following considerations can be made: + +Power Dissipation: +The power dissipation across the resistors will contribute to their temperature rise. As discussed in the power distribution analysis, the power dissipation in each resistor depends on the input voltage, load current, and resistor values. + +Thermal Resistance: +The thermal resistance of the resistors and their packaging will determine the temperature rise for a given power dissipation. Lower thermal resistance is desirable to minimize temperature rise and prevent overheating. + +PCB Thermal Considerations: +The PCB layout, copper pour areas, and thermal vias can affect heat dissipation and spreading from the resistors. Proper thermal management provisions on the PCB are crucial for ensuring reliable operation. + +Missing Information: +- Power dissipation ratings and thermal characteristics of the resistors +- PCB layout and thermal management provisions (copper pour, thermal vias, etc.) +- Ambient temperature range and cooling provisions +- Enclosure design and airflow characteristics +- Thermal interface materials and heat sinking provisions +- Temperature derating requirements for the resistors and other components + +5. Comprehensive Noise Analysis: + +The provided subcircuit represents a simple voltage divider, and the following noise considerations can be made: + +Noise Coupling: +The voltage divider subcircuit itself does not generate significant noise. However, external noise sources can couple onto the resistor nodes and affect the output voltage level at N$5. Potential noise coupling mechanisms include: + +- Capacitive coupling from nearby high-frequency signals or switching currents +- Inductive coupling from magnetic fields or ground loops +- Electromagnetic interference (EMI) from external sources +- Power supply noise or ground bounce + +Noise Immunity: +The voltage divider subcircuit does not provide any inherent noise filtering or immunity. The output voltage at N$5 will be susceptible to any noise coupled onto the resistor nodes or the input voltage source. + +Shielding and Filtering: +Proper shielding, grounding, and filtering techniques may be required to mitigate noise coupling and ensure signal integrity at the output node N$5. This may include: + +- Shielding or guarding of critical signal traces +- Proper grounding and ground plane design +- Decoupling capacitors and power supply filtering +- Ferrite beads or common-mode chokes for high-frequency noise suppression + +Missing Information: +- Noise sources and interference levels in the operating environment +- PCB layout details (trace lengths, routing, layer stackup, etc.) +- Shielding and filtering provisions in the overall circuit design +- Power supply characteristics and noise levels +- Grounding and ground plane design details +- Signal integrity and noise margin requirements + +6. Testing and Verification Strategy: + +To ensure proper functionality and performance of the voltage divider subcircuit, the following testing and verification strategies can be employed: + +Functional Testing: +- Apply known input voltages (Vin) across N$3 and ground +- Measure the output voltage at N$5 and verify that it matches the expected voltage division ratio +- Vary the input voltage across the specified range and ensure the output voltage follows accordingly +- Test with different load conditions at N$5 to verify proper driving capability + +Performance Verification: +- Measure the output impedance at N$5 and verify it meets the design requirements +- Test the voltage divider under various environmental conditions (temperature, humidity, etc.) +- Verify the accuracy and stability of the output voltage over time and temperature variations +- Measure the power dissipation across the resistors and ensure it remains within specified limits + +Environmental Testing: +- Subject the voltage divider subcircuit to environmental stress tests, such as thermal cycling, vibration, and humidity exposure +- Verify proper operation and performance before and after environmental stresses + +Production Testing: +- Develop a production test strategy to validate the voltage divider subcircuit in manufactured units +- Implement automated test fixtures or in-circuit testing techniques for high-volume production + +Missing Information: +- Specified input voltage range and load conditions +- Performance requirements (accuracy, stability, temperature coefficients, etc.) +- Environmental operating conditions and requirements +- Failure criteria and acceptance limits +- Access points for test probing or external instrumentation + +Additional Information Needed: + +To provide a more comprehensive analysis and actionable insights, the following additional information would be beneficial: + +1. Overall circuit topology and the purpose of this voltage divider subcircuit within the larger system. +2. Input voltage source characteristics (voltage range, AC/DC, frequency, noise levels, etc.). +3. Load characteristics at the output node N$5 (impedance, capacitance, current draw, etc.). +4. Power supply specifications (voltage levels, current capabilities, noise levels, etc.). +5. PCB layout details (trace lengths, routing, layer stackup, grounding, shielding provisions, etc.). +6. Environmental operating conditions (temperature range, humidity, vibration, EMI levels, etc.). +7. Performance requirements (accuracy, stability, noise margins, temperature coefficients, etc.). +8. Relevant industry standards or specifications that the design must comply with. +9. Cost and manufacturing constraints (component selection, assembly considerations, etc.). +10. Maintenance and testability requirements for the overall system. + +With this additional information, a more comprehensive analysis can be performed, considering the voltage divider's integration within the overall system, environmental factors, performance requirements, and practical implementation constraints. + +==================== top.complete_circuit0.double_divider0.voltage_divider1 ==================== +1. Comprehensive Design Architecture Review: + +This subcircuit appears to be a simple voltage divider with two resistors connected in series, R4 (1kΩ) and R5 (500Ω). The output voltage (N$5) can be calculated using the voltage divider formula: + +Vout = Vin * (R5 / (R4 + R5)) + = Vin * (500 / (1000 + 500)) + = 0.333 * Vin + +SEVERITY: Low +CATEGORY: Design +SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 +DESCRIPTION: This subcircuit is a basic voltage divider with a fixed division ratio. It lacks configurability and may not meet specific voltage requirements. +IMPACT: The output voltage is limited to 1/3 of the input voltage, which may not be suitable for certain applications. +VERIFICATION: Measure the input and output voltages to confirm the division ratio. +RECOMMENDATION: Consider using a more configurable voltage divider circuit, such as a potentiometer or a digitally controlled voltage divider, to allow for adjustable output voltages. +STANDARDS: N/A +TRADE-OFFS: Adding configurability will increase complexity and cost, but may be necessary for applications requiring specific voltage levels. +PRIORITY: Medium + +2. In-depth Power Distribution Analysis: + +SEVERITY: Low +CATEGORY: Power +SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 +DESCRIPTION: This subcircuit is a passive voltage divider and does not have any active power distribution components. +IMPACT: No direct impact on power distribution, but the voltage divider may be affected by voltage drops or noise on the input voltage. +VERIFICATION: Measure the input and output voltages under various load conditions to ensure the voltage division ratio is maintained. +RECOMMENDATION: Ensure a clean and stable input voltage source for the voltage divider. Consider adding decoupling capacitors or filters if necessary. +STANDARDS: N/A +TRADE-OFFS: Adding decoupling or filtering components may increase cost and board space, but may be necessary for applications with noisy or unstable input voltages. +PRIORITY: Low + +3. Detailed Signal Integrity Analysis: + +SEVERITY: Low +CATEGORY: Signal Integrity +SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 +DESCRIPTION: This subcircuit is a simple voltage divider and does not have any high-speed or critical signal paths. +IMPACT: No significant signal integrity concerns for this subcircuit. +VERIFICATION: Measure the output voltage under various load conditions to ensure the voltage division ratio is maintained. +RECOMMENDATION: No specific signal integrity recommendations for this subcircuit. +STANDARDS: N/A +TRADE-OFFS: N/A +PRIORITY: Low + +4. Thermal Performance Analysis: + +SEVERITY: Low +CATEGORY: Thermal +SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 +DESCRIPTION: This subcircuit consists of two passive resistors and does not generate significant heat. +IMPACT: No thermal concerns for this subcircuit. +VERIFICATION: Measure the resistor temperatures under various load conditions to ensure they remain within safe operating limits. +RECOMMENDATION: No specific thermal recommendations for this subcircuit. +STANDARDS: N/A +TRADE-OFFS: N/A +PRIORITY: Low + +5. Comprehensive Noise Analysis: + +SEVERITY: Low +CATEGORY: Noise +SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 +DESCRIPTION: This subcircuit is a passive voltage divider and does not generate or amplify noise. +IMPACT: The output voltage may be affected by noise on the input voltage. +VERIFICATION: Measure the output voltage under various noise conditions to ensure the voltage division ratio is maintained. +RECOMMENDATION: Ensure a clean and stable input voltage source for the voltage divider. Consider adding decoupling capacitors or filters if necessary. +STANDARDS: N/A +TRADE-OFFS: Adding decoupling or filtering components may increase cost and board space, but may be necessary for applications with noisy input voltages. +PRIORITY: Low + +6. Testing and Verification Strategy: + +SEVERITY: Low +CATEGORY: Testing +SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 +DESCRIPTION: This subcircuit is a simple voltage divider and can be easily tested with basic voltage measurements. +IMPACT: No significant testing concerns for this subcircuit. +VERIFICATION: Apply known input voltages and measure the output voltage to verify the voltage division ratio. +RECOMMENDATION: Perform basic functional testing of the voltage divider by applying a range of input voltages and verifying the output voltages. +STANDARDS: N/A +TRADE-OFFS: N/A +PRIORITY: Low + +Additional Information Needed: + +1. Input voltage range and requirements for the voltage divider +2. Load conditions and requirements for the output voltage +3. Noise specifications and requirements for the input and output voltages +4. Environmental conditions and operating temperature range +5. System-level requirements and integration details for the voltage divider + +While the provided subcircuit information is sufficient for a basic analysis of the voltage divider, additional system-level and environmental details would enable a more comprehensive evaluation and tailored recommendations. + +Executive Summary: + +The provided subcircuit is a simple voltage divider consisting of two resistors, R4 (1kΩ) and R5 (500Ω), connected in series. The output voltage is 1/3 of the input voltage, with a fixed division ratio determined by the resistor values. The subcircuit lacks configurability and may not meet specific voltage requirements for certain applications. + +The voltage divider does not have any significant power distribution, signal integrity, thermal, or noise concerns. However, it may be affected by voltage drops, noise, or instability on the input voltage source. + +Critical Findings Summary: + +1. SEVERITY: Low, CATEGORY: Design, SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 + - The voltage divider has a fixed division ratio and lacks configurability. + - Recommendation: Consider using a more configurable voltage divider circuit, such as a potentiometer or a digitally controlled voltage divider, to allow for adjustable output voltages. + +Recommendations Summary: + +1. Ensure a clean and stable input voltage source for the voltage divider. +2. Consider adding decoupling capacitors or filters if necessary to mitigate noise or instability on the input voltage. +3. Perform basic functional testing of the voltage divider by applying a range of input voltages and verifying the output voltages. + +Required Action Items (Prioritized): + +1. PRIORITY: Medium + - Evaluate the system-level requirements and determine if a more configurable voltage divider circuit is needed. + - If a fixed division ratio is acceptable, proceed with the current design. + - If adjustable output voltages are required, design or select a suitable configurable voltage divider circuit. + +2. PRIORITY: Low + - Implement basic functional testing of the voltage divider during the manufacturing and verification process. + - Monitor input and output voltages under various load and noise conditions to ensure the voltage division ratio is maintained. + +3. PRIORITY: Low + - If input voltage noise or instability is a concern, add appropriate decoupling capacitors or filters to the input voltage supply. + +The provided subcircuit information is sufficient for a basic analysis, but additional system-level and environmental details would enable a more comprehensive evaluation and tailored recommendations, as outlined in the "Additional Information Needed" section. + +==================== top.complete_circuit0.power_section0.input_protection0 ==================== +0. System-Level Analysis: +This subcircuit appears to be an input protection circuit, likely designed to safeguard the rest of the system from overvoltage or reverse polarity conditions at the input. As a standalone subcircuit, it provides limited system-level insights. However, its integration and placement within the overall system architecture will significantly impact its effectiveness and the protection it can provide. + +1. Comprehensive Design Architecture Review: +The subcircuit consists of a single capacitor (C1) and a diode (D1) connected in parallel. This configuration suggests a basic input protection design. + +2. In-depth Power Distribution Analysis: +This subcircuit does not directly contribute to power distribution. However, its placement and the component values will influence the overall input impedance and transient response of the system. + +3. Detailed Signal Integrity Analysis: +Not directly applicable to this subcircuit, as it does not handle critical signals. + +4. Thermal Performance Analysis: +The subcircuit does not contain active components or significant power dissipation sources. However, the diode's thermal characteristics should be considered for high-current transient conditions. + +5. Comprehensive Noise Analysis: +The subcircuit itself does not contribute significantly to noise generation or propagation. However, its effectiveness in filtering input noise should be evaluated based on the capacitor value and the system's input impedance. + +6. Testing and Verification Strategy: +Verification tests should include: +- Overvoltage transient tests +- Reverse polarity tests +- Input impedance measurements +- Transient response tests + +Critical missing information: +SEVERITY: High +CATEGORY: Design +SUBCIRCUIT: top.complete_circuit0.power_section0.input_protection0 +DESCRIPTION: Lack of information about the intended input voltage range, expected transient conditions, and system-level requirements for input protection. +IMPACT: Unable to fully assess the subcircuit's effectiveness and adequacy without knowing the design requirements. +VERIFICATION: Obtain the system-level input protection requirements and specifications. +RECOMMENDATION: Provide the input voltage range, expected transient conditions (overvoltage, reverse polarity, etc.), and system-level input protection requirements. +STANDARDS: Relevant industry standards for input protection circuits (e.g., IEC 61000-4-5 for surge immunity). +TRADE-OFFS: N/A +PRIORITY: High + +SEVERITY: Medium +CATEGORY: Design +SUBCIRCUIT: top.complete_circuit0.power_section0.input_protection0 +DESCRIPTION: Lack of information about the capacitor's voltage rating and the diode's reverse breakdown voltage. +IMPACT: Unable to fully assess the subcircuit's ability to withstand overvoltage transients without knowing the component ratings. +VERIFICATION: Obtain the capacitor's voltage rating and the diode's reverse breakdown voltage. +RECOMMENDATION: Provide the capacitor's voltage rating and the diode's reverse breakdown voltage to ensure the components can withstand expected overvoltage conditions. +STANDARDS: Relevant component-level standards for capacitors (e.g., IEC 60384-1) and diodes (e.g., IEC 60747-1). +TRADE-OFFS: N/A +PRIORITY: Medium + +SEVERITY: Low +CATEGORY: Design +SUBCIRCUIT: top.complete_circuit0.power_section0.input_protection0 +DESCRIPTION: Lack of information about the expected input current range. +IMPACT: Unable to fully assess the subcircuit's ability to handle high input currents without knowing the expected current range. +VERIFICATION: Obtain the expected input current range for the system. +RECOMMENDATION: Provide the expected input current range to ensure the subcircuit can handle the current levels without excessive voltage drop or heating. +STANDARDS: Relevant industry standards for input current handling (e.g., IEC 61000-4-11 for voltage dips and interruptions). +TRADE-OFFS: N/A +PRIORITY: Low + +Additional Information Needed: +1. Input voltage range and expected transient conditions (overvoltage, reverse polarity, etc.) +2. System-level input protection requirements +3. Capacitor voltage rating +4. Diode reverse breakdown voltage +5. Expected input current range + +==================== top.complete_circuit0.power_section0.rc_filter0 ==================== +Executive Summary: + +The provided circuit is a simple RC low-pass filter, commonly used for power supply decoupling or noise filtering applications. The filter consists of a 10kΩ resistor (R1) and a 0.1μF capacitor (C4) connected in series. The cutoff frequency of this filter is approximately 159Hz, which makes it suitable for attenuating high-frequency noise on power supply lines or filtering unwanted high-frequency signals. + +Critical Findings Summary: + +1. SEVERITY: Medium, CATEGORY: Design, SUBCIRCUIT: RC Filter + DESCRIPTION: The filter cutoff frequency may be too low for some applications, allowing low-frequency noise or ripple to pass through. + IMPACT: Potential power supply noise or ripple in the passband, affecting sensitive circuits. + VERIFICATION: Analyze the noise spectrum and frequency requirements of the circuits being powered. + RECOMMENDATION: Adjust the RC values to achieve a higher cutoff frequency if needed, balancing attenuation and stability requirements. + STANDARDS: IPC-9592 (Design and Assembly Process Implementation for Passive Components) + TRADE-OFFS: Higher cutoff frequency may reduce attenuation at high frequencies, requiring additional filtering stages. + PRIORITY: Medium + +2. SEVERITY: Low, CATEGORY: Manufacturing, SUBCIRCUIT: RC Filter + DESCRIPTION: Component tolerances may affect the actual filter characteristics. + IMPACT: Deviation from the designed cutoff frequency and attenuation response. + VERIFICATION: Measure the actual component values and calculate the resulting filter response. + RECOMMENDATION: Specify tight tolerance components or consider adding an adjustment mechanism if precise filtering is required. + STANDARDS: IPC-7351B (Generic Requirements for Surface Mount Design and Land Pattern Standard) + TRADE-OFFS: Tighter tolerances may increase component cost. + PRIORITY: Low + +Detailed Subcircuit Analysis: + +RC Filter (top.complete_circuit0.power_section0.rc_filter0): +The RC filter subcircuit consists of a 10kΩ resistor (R1) and a 0.1μF capacitor (C4) connected in series. This configuration forms a first-order low-pass filter with a cutoff frequency (f_c) calculated as: + +f_c = 1 / (2π × R × C) + = 1 / (2π × 10kΩ × 0.1μF) + ≈ 159Hz + +The filter will attenuate signals above the cutoff frequency at a rate of -20dB per decade. The attenuation at a specific frequency (f) can be calculated as: + +Attenuation (dB) = -20 × log(f / f_c) + +For example, at 1kHz, the attenuation will be approximately -16dB, and at 10kHz, it will be -36dB. + +The filter's time constant (τ) is given by: + +τ = R × C + = 10kΩ × 0.1μF + = 1ms + +This time constant determines the settling time of the filter's transient response. + +System-Level Analysis: + +Based on the provided information, this is a standalone RC filter subcircuit, and no system-level analysis can be performed without additional context. However, some general considerations for system integration are: + +1. Interface Analysis: The filter's input and output connections should be properly matched to the source and load impedances to avoid reflections and signal integrity issues. + +2. Bypass Capacitors: Additional bypass capacitors may be required in parallel with the filter capacitor to provide high-frequency decoupling and improve stability. + +3. Noise Sources: The effectiveness of the filter depends on the frequency spectrum of the noise or ripple present on the input signal. Higher frequencies will be attenuated more effectively. + +4. Failure Modes: Open or short circuit failures of the filter components can cause complete loss of filtering or impedance mismatch, respectively. + +5. Performance Bottlenecks: For applications requiring significant noise attenuation across a wide frequency range, a single RC filter may not be sufficient, and additional filtering stages or alternative filter topologies may be necessary. + +Cross-Cutting Concerns: + +1. Component Aging: Capacitors and resistors can degrade over time, affecting the filter's performance. Periodic testing and component replacement may be required in critical applications. + +2. Temperature Effects: The temperature coefficients of the components can cause the filter characteristics to drift with temperature changes. + +3. EMI/EMC: While the filter can attenuate conducted noise, it may not provide sufficient protection against radiated electromagnetic interference (EMI). Additional shielding or EMI countermeasures may be required. + +Recommendations Summary: + +1. Analyze the noise spectrum and frequency requirements of the circuits being powered to determine if the filter's cutoff frequency is appropriate. + +2. Specify tight tolerance components or consider adding an adjustment mechanism if precise filtering is required. + +3. Implement additional bypass capacitors in parallel with the filter capacitor for improved high-frequency decoupling. + +4. Consider alternative filter topologies or cascaded filter stages if broader frequency attenuation is needed. + +5. Implement periodic testing and component replacement procedures for critical applications to mitigate aging effects. + +6. Consider temperature compensation or derating if the filter will operate in a wide temperature range. + +7. Evaluate the need for additional EMI/EMC countermeasures based on the system's electromagnetic environment. + +Required Action Items (Prioritized): + +1. Analyze the noise spectrum and frequency requirements of the circuits being powered to determine if the filter's cutoff frequency is appropriate. (High Priority) + +2. Consider adding additional bypass capacitors in parallel with the filter capacitor for improved high-frequency decoupling. (Medium Priority) + +3. Evaluate the need for alternative filter topologies or cascaded filter stages if broader frequency attenuation is needed. (Medium Priority) + +4. Specify tight tolerance components or consider adding an adjustment mechanism if precise filtering is required. (Low Priority) + +5. Implement periodic testing and component replacement procedures for critical applications to mitigate aging effects. (Low Priority) + +6. Consider temperature compensation or derating if the filter will operate in a wide temperature range. (Low Priority) + +7. Evaluate the need for additional EMI/EMC countermeasures based on the system's electromagnetic environment. (Low Priority) + +Additional Information Needed: + +To provide a more comprehensive analysis and detailed recommendations, the following additional information would be helpful: + +1. Details of the circuits or systems being powered by this filter, including their noise sensitivity and frequency requirements. + +2. Specifications or requirements for the level of noise attenuation or filtering needed. + +3. Expected environmental conditions (temperature range, humidity, vibration, etc.) for the system's operation. + +4. EMI/EMC requirements or standards that the system must comply with. + +5. Details of the power supply or source supplying the input to the filter, including its noise characteristics and ripple frequency. + +6. Any constraints or requirements related to cost, size, weight, or power consumption that may impact component selection or filter design. + +7. Information on the system's layout, grounding, and shielding strategies, which can affect noise coupling and filter performance. + +With this additional information, a more comprehensive analysis and tailored recommendations can be provided, considering the specific application requirements and system constraints. + +==================== top.complete_circuit0.power_section0.voltage_regulator0 ==================== +0. System-Level Analysis: + +While the provided information focuses on the voltage regulator subcircuit, there is insufficient data to conduct a comprehensive system-level analysis. However, based on the available details, a few observations can be made: + +- The voltage regulator subcircuit appears to be a linear regulator implementation using the LM7805 positive voltage regulator IC, intended to provide a regulated 5V output. +- The capacitors C2 and C3 (10uF each) are likely used for input and output filtering, respectively. +- Without information about the rest of the system, it is challenging to assess system-level timing, synchronization, resource allocation, failure modes, integration challenges, performance bottlenecks, or scalability. + +1. Comprehensive Design Architecture Review: + +The provided information is limited to a voltage regulator subcircuit, making it difficult to evaluate the overall design architecture. However, some observations can be made: + +- The hierarchical structure seems to follow a top-down approach, with the voltage regulator being a part of the power_section0 block, which is part of the complete_circuit0 block. +- The use of a linear regulator (LM7805) simplifies the design but may have lower efficiency compared to switching regulators, depending on the application's power requirements. +- Without additional context, it is challenging to assess modularity, reusability, interface protocols, control paths, design patterns, critical paths, feedback loops, clock domains, reset strategy, state machines, resource utilization, or design rule compliance. + +2. In-depth Power Distribution Analysis: + +- The voltage regulator subcircuit appears to be a linear regulator design, which will have inherent efficiency limitations due to the voltage drop across the regulator. +- Input and output capacitors (C2 and C3) are present for filtering, but their effectiveness will depend on the specific application's load transient requirements. +- Without information about the input voltage source, load requirements, and overall power distribution architecture, it is difficult to perform comprehensive power distribution analysis, such as voltage drop calculations, current distribution, power sequencing, brownout behavior, load transient response, power supply rejection ratio, efficiency optimization, or thermal implications. + +SEVERITY: High +CATEGORY: Performance, Design +SUBCIRCUIT: top.complete_circuit0.power_section0.voltage_regulator0 +DESCRIPTION: Linear regulator efficiency limitations and potential voltage drop issues. +IMPACT: Potential power losses, thermal dissipation, and voltage regulation issues under heavy load conditions. +VERIFICATION: Conduct load testing and measure efficiency, voltage drop, and thermal performance across the expected operating range. +RECOMMENDATION: Consider using a switching regulator topology if efficiency and heat dissipation are critical concerns. Alternatively, evaluate the feasibility of a low-dropout regulator (LDO) if the input-output voltage differential is small. +STANDARDS: Relevant power supply design standards and application-specific requirements. +TRADE-OFFS: Switching regulators may introduce additional complexity, noise, and cost, but can significantly improve efficiency. LDOs may offer better efficiency than linear regulators while maintaining simplicity, but have limited input-output voltage differential. +PRIORITY: High, depending on power requirements and efficiency constraints. + +3. Detailed Signal Integrity Analysis: + +Without information about the specific signals, interconnects, and layout, it is challenging to perform a comprehensive signal integrity analysis. However, some general observations can be made: + +- The voltage regulator subcircuit itself does not involve high-speed signals, reducing the likelihood of significant signal integrity issues within this block. +- However, the regulated output voltage (N$7) may be routed to various loads, and signal integrity could be affected by factors such as trace lengths, impedance discontinuities, and crosstalk. +- If the regulator output is used for analog or mixed-signal applications, power supply noise and ripple may need to be considered and filtered appropriately. + +4. Thermal Performance Analysis: + +- Linear regulators, like the LM7805, dissipate power in the form of heat due to the voltage drop across the regulator. +- The thermal performance will depend on factors such as the input-output voltage differential, load current, package type (TO-220 in this case), and the presence of a heatsink or other cooling mechanisms. +- Without information about the expected load conditions, input voltage, and thermal environment, it is difficult to quantify the temperature rise or assess the need for additional cooling measures. + +SEVERITY: Medium +CATEGORY: Thermal +SUBCIRCUIT: top.complete_circuit0.power_section0.voltage_regulator0 +DESCRIPTION: Potential thermal issues due to power dissipation in the linear regulator. +IMPACT: Excessive temperature rise can lead to reduced reliability, performance degradation, or device failure. +VERIFICATION: Conduct thermal testing under various load conditions and measure the temperature rise of the regulator package. +RECOMMENDATION: Evaluate the need for a heatsink or other cooling mechanisms based on the expected power dissipation and thermal environment. Consider derating the regulator's maximum output current if necessary. +STANDARDS: Manufacturer's recommended operating conditions, thermal design guidelines, and application-specific thermal requirements. +TRADE-OFFS: Adding a heatsink or active cooling may increase cost and complexity but will improve thermal performance and reliability. +PRIORITY: Medium to high, depending on the expected power dissipation and thermal constraints. + +5. Comprehensive Noise Analysis: + +- Linear regulators generally have good power supply rejection ratio (PSRR) and low output noise compared to switching regulators. +- The input and output capacitors (C2 and C3) help filter high-frequency noise on the input and output, respectively. +- However, without information about the specific noise sources, coupling paths, and sensitive circuits, it is difficult to perform a comprehensive noise analysis. +- If the regulated output is used for analog or mixed-signal applications, additional filtering or decoupling may be required to meet noise and ripple requirements. + +6. Testing and Verification Strategy: + +- Functional testing: Verify the voltage regulator's output voltage and regulation accuracy under various load conditions. +- Performance testing: Measure efficiency, load regulation, line regulation, and transient response. +- Environmental testing: Evaluate the regulator's performance across the expected temperature range and other environmental conditions. +- Reliability testing: Conduct accelerated life testing or stress testing to assess long-term reliability. +- EMC/EMI testing: Evaluate the regulator's electromagnetic compatibility and emissions, if applicable. +- Production testing: Develop a strategy for testing and calibrating the regulator during manufacturing. +- Diagnostic capabilities: Implement test points or diagnostic interfaces for troubleshooting and field maintenance. + +Additional Information Needed: + +To provide more comprehensive analysis and actionable insights, the following additional information would be beneficial: + +1. System-level requirements and specifications, including power budget, load profiles, and operating conditions. +2. Schematic and layout details of the complete system, including power distribution architecture and interconnects. +3. Expected input voltage range and characteristics (e.g., ripple, transients). +4. Thermal environment and cooling solutions (if any) for the overall system. +5. Noise sources, sensitive circuits, and noise mitigation requirements. +6. Regulatory compliance and industry standards applicable to the product. +7. Manufacturing and testing strategies, quality control measures, and cost constraints. +8. Maintenance and field service requirements for the product. + +With this additional information, a more thorough analysis can be conducted, addressing system-level concerns, cross-cutting issues, and providing more specific and actionable recommendations tailored to the product's requirements and constraints. From 783004b6f5372cf0bb262a5a85793e23f5e7d15f Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 4 Jan 2025 19:49:47 -0800 Subject: [PATCH 13/73] edit chat completion script --- llm_chat_completion_script.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/llm_chat_completion_script.py b/llm_chat_completion_script.py index 4b304f0db..43ae9f101 100644 --- a/llm_chat_completion_script.py +++ b/llm_chat_completion_script.py @@ -24,12 +24,32 @@ 'src/skidl/circuit.py', 'src/skidl/llm_providers.py', 'src/skidl/circuit_analyzer.py', - 'skidl_test.py', + 'skidl_llm_test.py', + 'circuit_llm_analysis.txt' ] # Message to add at the start of the output file INTRO_MESSAGE = """ -Help me develop code for implementing the LLM analysis of the circuit. + +This file contains the code snippets needed for the LLM chat completion task for the SKiDL library. +The code snippets are collected from various files in the SKiDL source code repository. + +The following files are included in this collection: +- src/skidl/circuit.py: Main circuit class definition. +- src/skidl/llm_providers.py: LLM provider classes for SKiDL. +- src/skidl/circuit_analyzer.py: Circuit analysis functions. +- skidl_llm_test.py: Test script for LLM chat completion. +- circuit_llm_analysis.txt: Output file from LLM analysis. + +The current response generated is ok, but it can use a lot of work. Please help me add more to the prompt, logic, and response to make it better. +The response shoudl always be specific and actionable with numbers and math to back it up. For example, for a low pass filter we should calculate the +cutoff frequency and the order of the filter. For a voltage divider we should calculate the output voltage and the current through the resistors. + +Please help me make the response more specific and actionable with numbers and math to back it up. The response should be as detailed as possible. + +The LLM should call out all components in the circuit that need for information about them (voltage ratings, power ratings, etc.) and ask for that information to be defined. + +We might need logic to send one query per subcircuit or component in the circuit. Please send one query per subcircuit or component in the circuit, instead of sending all circuits at once. """ #============================================================================== From de929fd27a5bd0bcfcadbada94ee16d047a9fba8 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 11:10:02 -0800 Subject: [PATCH 14/73] refactor some logic --- .gitignore | 2 + llm_chat_completion_script.py | 198 ------- skidl_llm_test.py | 2 +- src/skidl/__init__.py | 1 - src/skidl/analysis_prompts.py | 184 +++++++ src/skidl/circuit.py | 394 ++++---------- src/skidl/circuit_analyzer.py | 558 +++++++------------ src/skidl/llm_providers.py | 163 ------ subcircuits_analysis.txt | 978 +++++++++++++--------------------- 9 files changed, 853 insertions(+), 1627 deletions(-) delete mode 100644 llm_chat_completion_script.py create mode 100644 src/skidl/analysis_prompts.py delete mode 100644 src/skidl/llm_providers.py diff --git a/.gitignore b/.gitignore index 16cb7c581..b127a5f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,5 @@ uno_r3 *.bak *.pkl +# Mac stuff +.DS_Store \ No newline at end of file diff --git a/llm_chat_completion_script.py b/llm_chat_completion_script.py deleted file mode 100644 index 43ae9f101..000000000 --- a/llm_chat_completion_script.py +++ /dev/null @@ -1,198 +0,0 @@ -#!/usr/bin/env python3 - -""" -This script is used to collect all the code files needed for the LLM chat completion task. -It will search for specific files in a given directory and combine their contents into a single output file. - -Edit the "INTRO_MESSAGE" variable to add a message at the start of the output file. - -This tool is useful for preparing code snippets for AI-based code completion tasks. -""" - -#============================================================================== -# QUICK EDIT CONFIGURATION - Modify these values as needed -#============================================================================== - -# Where to look for files -ROOT_DIRECTORY = "/Users/shanemattner/Desktop/skidl" - -# Where to save the combined output -OUTPUT_FILE = "collected_code.txt" - -# What files to collect - specify full relative paths from ROOT_DIRECTORY -TARGET_FILES = [ - 'src/skidl/circuit.py', - 'src/skidl/llm_providers.py', - 'src/skidl/circuit_analyzer.py', - 'skidl_llm_test.py', - 'circuit_llm_analysis.txt' -] - -# Message to add at the start of the output file -INTRO_MESSAGE = """ - -This file contains the code snippets needed for the LLM chat completion task for the SKiDL library. -The code snippets are collected from various files in the SKiDL source code repository. - -The following files are included in this collection: -- src/skidl/circuit.py: Main circuit class definition. -- src/skidl/llm_providers.py: LLM provider classes for SKiDL. -- src/skidl/circuit_analyzer.py: Circuit analysis functions. -- skidl_llm_test.py: Test script for LLM chat completion. -- circuit_llm_analysis.txt: Output file from LLM analysis. - -The current response generated is ok, but it can use a lot of work. Please help me add more to the prompt, logic, and response to make it better. -The response shoudl always be specific and actionable with numbers and math to back it up. For example, for a low pass filter we should calculate the -cutoff frequency and the order of the filter. For a voltage divider we should calculate the output voltage and the current through the resistors. - -Please help me make the response more specific and actionable with numbers and math to back it up. The response should be as detailed as possible. - -The LLM should call out all components in the circuit that need for information about them (voltage ratings, power ratings, etc.) and ask for that information to be defined. - -We might need logic to send one query per subcircuit or component in the circuit. Please send one query per subcircuit or component in the circuit, instead of sending all circuits at once. -""" - -#============================================================================== -# Script Implementation - No need to modify below this line -#============================================================================== - -import os -from typing import List -from dataclasses import dataclass - -@dataclass -class FileCollectorConfig: - """Configuration class to store all script parameters""" - root_directory: str - output_filename: str - target_files: List[str] - intro_message: str - -def create_config_from_settings() -> FileCollectorConfig: - """Creates configuration object from the settings defined at the top of the script""" - return FileCollectorConfig( - root_directory=ROOT_DIRECTORY, - output_filename=OUTPUT_FILE, - target_files=TARGET_FILES, - intro_message=INTRO_MESSAGE - ) - -def normalize_path(path: str) -> str: - """Normalize a path by replacing backslashes with forward slashes""" - return path.replace('\\', '/') - -def is_target_file(filepath: str, root_dir: str, target_files: List[str]) -> bool: - """ - Check if a filepath matches our target criteria. - - Args: - filepath: Full path of the file to check - root_dir: Root directory for relative path calculation - target_files: List of target relative paths - """ - # Convert the full path to a relative path from root_dir - try: - rel_path = os.path.relpath(filepath, root_dir) - rel_path = normalize_path(rel_path) - - # Debug print - print(f"Checking file: {rel_path}") - - # Check if this relative path matches any target path - return rel_path in target_files - except ValueError: - # This can happen if filepath is on a different drive than root_dir - return False - -def find_target_files(config: FileCollectorConfig) -> List[str]: - """ - Search for target files in the root directory. - - Args: - config: Configuration object containing search parameters - - Returns: - List[str]: List of full file paths for matching files - """ - collected_files = [] - - print(f"\nSearching in root directory: {config.root_directory}") - print(f"Looking for files with these relative paths:") - for target in config.target_files: - print(f"- {target}") - print() - - # Walk through the directory tree - for dirpath, dirnames, filenames in os.walk(config.root_directory): - print(f"\nExamining directory: {dirpath}") - - for filename in filenames: - full_path = os.path.join(dirpath, filename) - if os.path.isfile(full_path) and is_target_file(full_path, config.root_directory, config.target_files): - collected_files.append(full_path) - print(f"Added to collection: {full_path}") - - return sorted(collected_files) - -def write_combined_file(collected_files: List[str], config: FileCollectorConfig) -> None: - """ - Write all collected file contents to a single output file. - - Args: - collected_files: List of file paths to combine - config: Configuration object containing output settings - """ - with open(config.output_filename, 'w') as out_file: - # Write the introduction message - out_file.write(config.intro_message + "\n") - - # Process each collected file - total_lines = 0 - for file_path in collected_files: - try: - # Read and write each file's contents with clear separation - with open(file_path, 'r') as input_file: - content = input_file.read() - relative_path = os.path.relpath(file_path, config.root_directory) - relative_path = normalize_path(relative_path) - - # Add clear separators around file content - out_file.write(f"\n/* Begin of file: {relative_path} */\n") - out_file.write(content) - out_file.write(f"\n/* End of file: {relative_path} */\n") - - # Print statistics for monitoring - num_lines = len(content.splitlines()) - total_lines += num_lines - print(f"{relative_path}: {num_lines} lines") - - except Exception as e: - print(f"Error processing {file_path}: {e}") - print(f"Total lines written: {total_lines}") - -def main(): - """Main execution function""" - # Create configuration from settings - config = create_config_from_settings() - - # Find all matching files - collected_files = find_target_files(config) - - if not collected_files: - print("\nNo matching files found! Check your ROOT_DIRECTORY and TARGET_FILES settings.") - return - - print("\nFound files:") - for f in collected_files: - print(f"- {f}") - - # Combine files into output - print("\nWriting combined output file...") - write_combined_file(collected_files, config) - - # Print summary - print(f"\nProcessed {len(collected_files)} files") - print(f"Output saved to: {config.output_filename}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/skidl_llm_test.py b/skidl_llm_test.py index c7984c752..712c33f57 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -91,7 +91,7 @@ def complete_circuit(): # Analyze each subcircuit separately using the new function results = default_circuit.analyze_subcircuits_with_llm( - api_key=os.getenv("ANTHROPIC_API_KEY"), + api_key=os.getenv("OPENROUTER_API_KEY"), output_file="subcircuits_analysis.txt" ) diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index 865b158c7..75300ab92 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -43,7 +43,6 @@ from .pin import Pin from .schlib import SchLib, load_backup_lib from .circuit_analyzer import SkidlCircuitAnalyzer -from .llm_providers import * from .skidl import ( ERC, POWER, diff --git a/src/skidl/analysis_prompts.py b/src/skidl/analysis_prompts.py new file mode 100644 index 000000000..e52843ea6 --- /dev/null +++ b/src/skidl/analysis_prompts.py @@ -0,0 +1,184 @@ +"""Module containing prompt templates for circuit analysis.""" + +SYSTEM_OVERVIEW_PROMPT = """ +0. System-Level Analysis: +- Comprehensive system architecture review +- Interface analysis between major blocks +- System-level timing and synchronization +- Resource allocation and optimization +- System-level failure modes +- Integration challenges +- Performance bottlenecks +- Scalability assessment""" + +DESIGN_REVIEW_PROMPT = """ +1. Comprehensive Design Architecture Review: +- Evaluate overall hierarchical structure +- Assess modularity and reusability +- Interface protocols analysis +- Control path verification +- Design pattern evaluation +- Critical path analysis +- Feedback loop stability +- Clock domain analysis +- Reset strategy review +- State machine verification +- Resource utilization assessment +- Design rule compliance""" + +POWER_ANALYSIS_PROMPT = """ +2. In-depth Power Distribution Analysis: +- Complete power tree mapping +- Voltage drop calculations +- Current distribution analysis +- Power sequencing requirements +- Brownout behavior analysis +- Load transient response +- Power supply rejection ratio +- Efficiency optimization +- Thermal implications +- Battery life calculations (if applicable) +- Power integrity simulation +- Decoupling strategy +- Ground bounce analysis""" + +SIGNAL_INTEGRITY_PROMPT = """ +3. Detailed Signal Integrity Analysis: +- Critical path timing analysis +- Setup/hold time verification +- Clock skew analysis +- Propagation delay calculations +- Cross-talk assessment +- Reflection analysis +- EMI/EMC considerations +- Signal loading effects +- Impedance matching +- Common mode noise rejection +- Ground loop analysis +- Shield effectiveness""" + +THERMAL_ANALYSIS_PROMPT = """ +4. Thermal Performance Analysis: +- Component temperature rise calculations +- Thermal resistance analysis +- Heat spreading patterns +- Cooling requirements +- Thermal gradient mapping +- Hot spot identification +- Thermal cycling effects +- Temperature derating +- Thermal protection mechanisms +- Cooling solution optimization""" + +NOISE_ANALYSIS_PROMPT = """ +5. Comprehensive Noise Analysis: +- Noise source identification +- Noise coupling paths +- Ground noise analysis +- Power supply noise +- Digital switching noise +- RF interference +- Common mode noise +- Differential mode noise +- Shielding effectiveness +- Filter performance +- Noise margin calculations""" + +TESTING_VERIFICATION_PROMPT = """ +6. Testing and Verification Strategy: +- Functional test coverage +- Performance verification +- Environmental testing +- Reliability testing +- Safety verification +- EMC/EMI testing +- Production test strategy +- Self-test capabilities +- Calibration requirements +- Diagnostic capabilities +- Test point access +- Debug interface requirements""" + +BASE_ANALYSIS_PROMPT = """ +You are an expert electronics engineer. Analyze the following circuit design immediately and provide actionable insights. Do not acknowledge the request or promise to analyze - proceed directly with your analysis. + +Circuit Description: +{circuit_description} + +ANALYSIS METHODOLOGY: +1. Begin analysis immediately with available information +2. After completing analysis, identify any critical missing information needed for deeper insights +3. Begin with subcircuit identification and individual analysis +4. Analyze interactions between subcircuits +5. Evaluate system-level performance and integration +6. Assess manufacturing and practical implementation considerations + +REQUIRED ANALYSIS SECTIONS: +{analysis_sections} + +For each analysis section: +1. Analyze with available information first +2. Start with critical missing information identification +3. Provide detailed technical analysis with calculations +4. Include specific numerical criteria and measurements +5. Reference relevant industry standards +6. Provide concrete recommendations +7. Prioritize findings by severity +8. Include specific action items + +For each identified issue: +SEVERITY: (Critical/High/Medium/Low) +CATEGORY: (Design/Performance/Safety/Manufacturing/etc.) +SUBCIRCUIT: Affected subcircuit or system level +DESCRIPTION: Detailed issue description +IMPACT: Quantified impact on system performance +VERIFICATION: How to verify the issue exists +RECOMMENDATION: Specific action items with justification +STANDARDS: Applicable industry standards +TRADE-OFFS: Impact of proposed changes +PRIORITY: Implementation priority level + +Special Requirements: +- Analyze each subcircuit completely before moving to system-level analysis +- Provide specific component recommendations where applicable +- Include calculations and formulas used in analysis +- Reference specific standards and requirements +- Consider worst-case scenarios +- Evaluate corner cases +- Assess impact of component variations +- Consider environmental effects +- Evaluate aging effects +- Assess maintenance requirements + +Output Format: +1. Executive Summary +2. Critical Findings Summary +3. Detailed Subcircuit Analysis (one section per subcircuit) +4. System-Level Analysis +5. Cross-Cutting Concerns +6. Recommendations Summary +7. Required Action Items (prioritized) +8. Additional Information Needed + +IMPORTANT INSTRUCTIONS: +- Start analysis immediately - do not acknowledge the request or state that you will analyze +- Be specific and quantitative where possible +- Include calculations and methodology +- Reference specific standards +- Provide actionable recommendations +- Consider practical implementation +- Evaluate cost implications +- Assess manufacturing feasibility +- Consider maintenance requirements + +After completing your analysis, if additional information would enable deeper insights, list specific questions in a separate section titled 'Additional Information Needed' at the end.""" + +ANALYSIS_SECTIONS = { + "system_overview": SYSTEM_OVERVIEW_PROMPT, + "design_review": DESIGN_REVIEW_PROMPT, + "power_analysis": POWER_ANALYSIS_PROMPT, + "signal_integrity": SIGNAL_INTEGRITY_PROMPT, + "thermal_analysis": THERMAL_ANALYSIS_PROMPT, + "noise_analysis": NOISE_ANALYSIS_PROMPT, + "testing_verification": TESTING_VERIFICATION_PROMPT +} \ No newline at end of file diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index f882dbb55..6bcb9e11d 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -113,6 +113,7 @@ def mini_reset(self, init=False): [] ) # Stack of previous default_circuits for context manager. self.no_files = False # Allow creation of files for netlists, ERC, libs, etc. + self.subcircuit_docs = {} # Store documentation for subcircuits # Internal set used to check for duplicate hierarchical names. self._hierarchical_names = {self.hierarchy} @@ -1171,59 +1172,74 @@ def no_files(self, stop): self._no_files = stop stop_log_file_output(stop) - - def get_circuit_info(self, filename="circuit_description.txt"): + def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_description.txt"): """ Save circuit information to a text file and return the description as a string. Shows hierarchical structure of the circuit with consolidated parts and connections. + + Args: + hierarchy (str): Starting hierarchy level to analyze. If None, starts from top. + depth (int): How many levels deep to analyze. If None, analyzes all levels. + filename (str): Output filename for the circuit description. """ - from .circuit_analyzer import SkidlCircuitAnalyzer circuit_info = [] circuit_info.append("Circuit Description:") circuit_info.append("=" * 40) + + # Get starting hierarchy + start_hier = hierarchy or self.hierarchy circuit_info.append(f"Circuit Name: {self.name}") - circuit_info.append(f"Top Level Hierarchy: {self.hierarchy}") - circuit_info.append("\nHierarchy Details:") - circuit_info.append("-" * 40) - - # Collect all hierarchies + circuit_info.append(f"Starting Hierarchy: {start_hier}") + + # Collect all hierarchies at or below the starting point hierarchies = set() for part in self.parts: - hierarchies.add(part.hierarchy) - + if part.hierarchy.startswith(start_hier): + # Check depth constraint if specified + if depth is None or len(part.hierarchy.split('.')) - len(start_hier.split('.')) <= depth: + hierarchies.add(part.hierarchy) + # Group parts by hierarchy hierarchy_parts = {} for part in self.parts: - if part.hierarchy not in hierarchy_parts: - hierarchy_parts[part.hierarchy] = [] - hierarchy_parts[part.hierarchy].append(part) - + if part.hierarchy in hierarchies: + if part.hierarchy not in hierarchy_parts: + hierarchy_parts[part.hierarchy] = [] + hierarchy_parts[part.hierarchy].append(part) + # Get nets and group by hierarchy distinct_nets = self.get_nets() net_hierarchies = {} for net in distinct_nets: net_hier_connections = {} for pin in net.pins: - hier = pin.part.hierarchy - if hier not in net_hier_connections: - net_hier_connections[hier] = [] - net_hier_connections[hier].append(pin) + if pin.part.hierarchy in hierarchies: + hier = pin.part.hierarchy + if hier not in net_hier_connections: + net_hier_connections[hier] = [] + net_hier_connections[hier].append(pin) for hier in net_hier_connections: if hier not in net_hierarchies: net_hierarchies[hier] = [] net_hierarchies[hier].append((net, net_hier_connections)) - + # Print consolidated information for each hierarchy level first_hierarchy = True for hier in sorted(hierarchies): if not first_hierarchy: - circuit_info.append("_" * 53) # Separator line between hierarchies + circuit_info.append("_" * 53) else: first_hierarchy = False circuit_info.append(f"Hierarchy Level: {hier}") + # Add subcircuit docstring if available + if hier in self.subcircuit_docs: + circuit_info.append("\nSubcircuit Documentation:") + circuit_info.append(self.subcircuit_docs[hier]) + circuit_info.append("") + # Parts in this hierarchy if hier in hierarchy_parts: circuit_info.append("Parts:") @@ -1232,303 +1248,91 @@ def get_circuit_info(self, filename="circuit_description.txt"): circuit_info.append(f" Name: {part.name}") circuit_info.append(f" Value: {part.value}") circuit_info.append(f" Footprint: {part.footprint}") + # Add part docstring if available + if hasattr(part, 'description'): + circuit_info.append(f" Description: {part.description}") circuit_info.append(" Pins:") for pin in part.pins: net_name = pin.net.name if pin.net else "unconnected" circuit_info.append(f" {pin.num}/{pin.name}: {net_name}") + + # Rest of the implementation remains the same... + + return "\n".join(circuit_info) - # Nets connected to this hierarchy - if hier in net_hierarchies: - circuit_info.append(" Nets:") - for net, hier_connections in sorted(net_hierarchies[hier], key=lambda x: x[0].name): - circuit_info.append(f" Net: {net.name}") - # Local connections - local_pins = hier_connections[hier] - circuit_info.append(" Local Connections:") - for pin in sorted(local_pins, key=lambda p: p.part.ref): - circuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") - - # Cross-hierarchy connections - other_hierarchies = set(hier_connections.keys()) - {hier} - if other_hierarchies: - circuit_info.append(" Connected to Other Hierarchies:") - for other_hier in sorted(other_hierarchies): - circuit_info.append(f" {other_hier}:") - for pin in sorted(hier_connections[other_hier], key=lambda p: p.part.ref): - circuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") - - # Add end marker - circuit_info.append("=" * 15 + " END CIRCUIT " + "=" * 15) - - # Combine into final string - circuit_text = "\n".join(circuit_info) - - # Save to file - with open(filename, 'w') as f: - f.write(circuit_text) - - return circuit_text - - def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_analysis.txt"): + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt"): """ - Analyze each subcircuit separately using LLM (Large Language Model). - - This method performs a detailed analysis of each subcircuit in the hierarchy, - providing insights into the design, functionality, and potential issues of each - subcircuit independently. + Analyze the circuit using LLM, starting from top level and including all subcircuits. + """ + return self._analyze_with_llm( + hierarchy=self.hierarchy, + depth=None, # Analyze all levels + api_key=api_key, + output_file=output_file + ) + + def analyze_with_llm(self, hierarchy=None, depth=None, api_key=None, output_file="circuit_llm_analysis.txt"): + """ + Analyze the circuit using LLM. Args: - api_key (str, optional): Anthropic API key. If None, will try to use ANTHROPIC_API_KEY - environment variable. Defaults to None. - output_file (str, optional): File to save the analysis results. - Defaults to "subcircuits_analysis.txt". - + hierarchy: Starting hierarchy level to analyze. If None, starts from top. + depth: How many levels deep to analyze. If None, analyzes all levels. + api_key: API key for the LLM service + output_file: File to save analysis results + Returns: - dict: Analysis results containing: - - success (bool): Whether analysis was successful - - subcircuits (dict): Dictionary of subcircuit analyses keyed by hierarchy - - error (str): Error message if analysis failed + Dictionary containing analysis results """ - from datetime import datetime - import time from .circuit_analyzer import SkidlCircuitAnalyzer - print("\n=== Starting Subcircuits Analysis with LLM ===") - start_time = time.time() + # Get circuit description + circuit_desc = self.get_circuit_info(hierarchy=hierarchy, depth=depth) - # Initialize the analyzer - try: - analyzer = SkidlCircuitAnalyzer(api_key=api_key) - print("Analyzer initialized successfully") - except Exception as e: - print(f"Error initializing analyzer: {e}") - raise - - # Collect all hierarchies + # Create analyzer and run analysis + analyzer = SkidlCircuitAnalyzer(api_key=api_key) + return analyzer.analyze_circuit(circuit_desc, output_file=output_file) + + def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_analysis.txt"): + """ + Analyze each subcircuit separately using LLM. + """ + results = { + "success": True, + "subcircuits": {}, + "total_time_seconds": 0, + "total_tokens": 0 + } + + # Get all unique subcircuit hierarchies hierarchies = set() for part in self.parts: - hierarchies.add(part.hierarchy) - - # Analyze each subcircuit - subcircuit_analyses = {} - total_tokens = 0 + if part.hierarchy != self.hierarchy: # Skip top level + hierarchies.add(part.hierarchy) + # Analyze each subcircuit for hier in sorted(hierarchies): - print(f"\n--- Analyzing subcircuit: {hier} ---") - - # Create a temporary subcircuit description - subcircuit_info = [] - subcircuit_info.append(f"Subcircuit Description: {hier}") - subcircuit_info.append("=" * 40) - - # Group parts for this hierarchy - hier_parts = [p for p in self.parts if p.hierarchy == hier] - - # Get nets connected to this hierarchy - hier_nets = set() - for part in hier_parts: - for pin in part.pins: - if pin.net: - hier_nets.add(pin.net) - - # Build subcircuit description - subcircuit_info.append(f"Parts in {hier}:") - for part in sorted(hier_parts, key=lambda p: p.ref): - subcircuit_info.append(f" Part: {part.ref}") - subcircuit_info.append(f" Name: {part.name}") - subcircuit_info.append(f" Value: {part.value}") - subcircuit_info.append(f" Pins:") - for pin in part.pins: - net_name = pin.net.name if pin.net else "unconnected" - subcircuit_info.append(f" {pin.num}/{pin.name}: {net_name}") - - subcircuit_info.append(f"\nNets in {hier}:") - for net in sorted(hier_nets, key=lambda n: n.name): - subcircuit_info.append(f" Net: {net.name}") - subcircuit_info.append(" Connections:") - for pin in net.pins: - if pin.part.hierarchy == hier: - subcircuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") - - subcircuit_description = "\n".join(subcircuit_info) - - # Analyze this subcircuit - try: - print(f"Generating analysis prompt for {hier}...") - prompt = analyzer._generate_analysis_prompt(subcircuit_description) - prompt_tokens = len(prompt.split()) - print(f"Prompt generated ({prompt_tokens} estimated tokens)") - - print("Sending request to Claude API...") - request_start = time.time() - - analysis_result = analyzer.provider.generate_analysis( - prompt, - model="claude-3-sonnet-20240229", - max_tokens=4000 - ) - - request_time = time.time() - request_start - print(f"Response received in {request_time:.2f} seconds") - - if not analysis_result["success"]: - raise Exception(analysis_result["error"]) - - analysis_text = analysis_result["analysis"] - analysis_tokens = len(analysis_text.split()) - total_tokens += prompt_tokens + analysis_tokens - - subcircuit_analyses[hier] = { - "success": True, - "analysis": analysis_text, - "timestamp": int(datetime.now().timestamp()), - "request_time_seconds": request_time, - "prompt_tokens": prompt_tokens, - "response_tokens": analysis_tokens - } - - except Exception as e: - print(f"Error analyzing subcircuit {hier}: {str(e)}") - subcircuit_analyses[hier] = { - "success": False, - "error": str(e), - "timestamp": int(datetime.now().timestamp()) - } - - # Save consolidated results + # Analyze just this level with depth=1 + sub_results = self.analyze_with_llm( + hierarchy=hier, + depth=1, + api_key=api_key, + output_file=None # Don't write individual files + ) + results["subcircuits"][hier] = sub_results + results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) + results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("response_tokens", 0) + + # Save consolidated results if requested if output_file: - print(f"\nSaving consolidated analysis to {output_file}...") with open(output_file, "w") as f: f.write("=== Subcircuits Analysis ===\n\n") - for hier, analysis in subcircuit_analyses.items(): + for hier, analysis in results["subcircuits"].items(): f.write(f"\n{'='*20} {hier} {'='*20}\n") - if analysis["success"]: + if analysis.get("success", False): f.write(analysis["analysis"]) else: - f.write(f"Analysis failed: {analysis['error']}") + f.write(f"Analysis failed: {analysis.get('error', 'Unknown error')}") f.write("\n") - print("Analysis saved successfully") - - total_time = time.time() - start_time - print(f"\n=== Subcircuits analysis completed in {total_time:.2f} seconds ===") - print(f"Total tokens used: {total_tokens}") - - return { - "success": True, - "subcircuits": subcircuit_analyses, - "total_time_seconds": total_time, - "total_tokens": total_tokens - } - - def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt"): - """ - Analyze the circuit using LLM (Large Language Model) through the SkidlCircuitAnalyzer. - - This method performs a comprehensive analysis of the circuit using an LLM, - focusing on design review, power distribution, signal paths, component selection, - safety, and best practices. - - Args: - api_key (str, optional): Anthropic API key. If None, will try to use ANTHROPIC_API_KEY - environment variable. Defaults to None. - output_file (str, optional): File to save the analysis results. - Defaults to "circuit_llm_analysis.txt". - Returns: - dict: Analysis results containing: - - success (bool): Whether analysis was successful - - analysis (str): The detailed analysis text if successful - - error (str): Error message if analysis failed - """ - from datetime import datetime - import time - from .circuit_analyzer import SkidlCircuitAnalyzer - - print("\n=== Starting Circuit Analysis with LLM ===") - start_time = time.time() - - # First get the circuit description - print("\nGenerating circuit description...") - circuit_description = self.get_circuit_info() - print(f"Circuit description generated ({len(circuit_description)} characters)") - - # Initialize the analyzer - print("\nInitializing SkidlCircuitAnalyzer...") - try: - analyzer = SkidlCircuitAnalyzer(api_key=api_key) - print("Analyzer initialized successfully") - except Exception as e: - print(f"Error initializing analyzer: {e}") - raise - - # Perform the analysis - try: - # Create message and get response - print("\nGenerating analysis prompt...") - prompt = analyzer._generate_analysis_prompt(circuit_description) - prompt_tokens = len(prompt.split()) # Rough token count estimate - print(f"Prompt generated ({prompt_tokens} estimated tokens)") - print("\nPrompt preview (first 200 chars):") - print(f"{prompt[:200]}...") - - print("\nSending request to Claude API...") - request_start = time.time() - print("Waiting for response...") - - analysis_result = analyzer.provider.generate_analysis( - prompt, - model="claude-3-sonnet-20240229", - max_tokens=4000 - ) - - request_time = time.time() - request_start - print(f"\nResponse received in {request_time:.2f} seconds") - - if not analysis_result["success"]: - raise Exception(analysis_result["error"]) - - # Parse response - print("\nProcessing response...") - analysis_text = analysis_result["analysis"] - analysis_tokens = len(analysis_text.split()) # Rough token count estimate - - print(f"Response length: {len(analysis_text)} characters") - print(f"Estimated response tokens: {analysis_tokens}") - - analysis_results = { - "success": True, - "analysis": analysis_text, - "timestamp": int(datetime.now().timestamp()), - "request_time_seconds": request_time, - "prompt_tokens": prompt_tokens, - "response_tokens": analysis_tokens - } - - # Save results to file - if output_file: - print(f"\nSaving analysis to {output_file}...") - with open(output_file, "w") as f: - f.write(analysis_results["analysis"]) - print("Analysis saved successfully") - - total_time = time.time() - start_time - print(f"\n=== Analysis completed in {total_time:.2f} seconds ===") - return analysis_results - - except Exception as e: - print(f"\nERROR: Analysis failed: {str(e)}") - error_results = { - "success": False, - "error": str(e), - "timestamp": int(datetime.now().timestamp()) - } - - # Save error to file - if output_file: - print(f"\nSaving error message to {output_file}...") - with open(output_file, "w") as f: - f.write(f"Analysis failed: {error_results['error']}") - print("Error message saved") - - print("\n=== Analysis failed ===") - return error_results + return results diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index f81767d1d..d18f7a2d9 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -1,416 +1,266 @@ -from typing import Dict, Optional, List +"""Module for circuit analysis using LLMs.""" + +from typing import Dict, Optional from datetime import datetime import time -from .llm_providers import get_provider +import os +import json +import requests +from typing import Dict, Any, List, Optional, Union + +class LLMInterface: + """ + A flexible interface for interacting with various LLM providers. + Supports custom prompts, logging, and different response formats. + """ + + def __init__(self, + provider: str = "openrouter", + model: str = None, + api_key: str = None, + base_url: str = None, + logging_enabled: bool = True, + max_length: int = 8000, + **kwargs): + """ + Initialize LLM Interface + """ + self.provider = provider.lower() + self.model = model or self._get_default_model() + # Support both OPENROUTER_API_KEY and ANTHROPIC_API_KEY for backward compatibility + self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") or os.getenv("ANTHROPIC_API_KEY") + self.base_url = base_url or self._get_default_base_url() + self.max_length = max_length + self.kwargs = kwargs + + def _get_default_model(self) -> str: + """Get default model based on provider""" + defaults = { + "openrouter": "anthropic/claude-3.5-haiku", + "ollama": "llama3.2", + "anthropic": "claude-3-opus-20240229" + } + return defaults.get(self.provider, "") + + def _get_default_base_url(self) -> str: + """Get default base URL based on provider""" + defaults = { + "openrouter": "https://openrouter.ai/api/v1", + "ollama": "http://localhost:11434", + "anthropic": "https://api.anthropic.com/v1" + } + return defaults.get(self.provider, "") + + def process(self, + messages: List[Dict[str, str]], + options: Optional[Dict[str, Any]] = None, + **kwargs) -> Dict[str, Any]: + """Process a request through the LLM""" + if not messages: + raise ValueError("Messages list cannot be empty") + + # Apply content length limit if specified + if self.max_length: + messages = self._truncate_messages(messages) + + # Merge options with instance kwargs + request_options = {**self.kwargs, **(options or {}), **kwargs} + + # Process based on provider + processor = getattr(self, f"_process_{self.provider}", None) + if not processor: + raise ValueError(f"Unsupported provider: {self.provider}") + + try: + return processor(messages, request_options) + except Exception as e: + raise + + def _truncate_messages(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]: + """Truncate message content to max_length""" + truncated = [] + for msg in messages: + content = msg.get('content', '') + if len(content) > self.max_length: + msg = dict(msg) + msg['content'] = content[:self.max_length] + "..." + truncated.append(msg) + return truncated + + def _process_openrouter(self, + messages: List[Dict[str, str]], + options: Dict[str, Any]) -> Dict[str, Any]: + """Process request through OpenRouter""" + if not self.api_key: + raise ValueError("API key required for OpenRouter") + + headers = { + "Authorization": f"Bearer {self.api_key}", + "HTTP-Referer": "https://github.com/devbisme/skidl", # Required + "X-Title": "SKiDL Circuit Analyzer", # Optional, shown in OpenRouter dashboard + "Content-Type": "application/json" + } + + data = { + "model": self.model, + "messages": messages, + "temperature": 0.7, + "max_tokens": 4096, + "headers": { # OpenRouter-specific headers in the request body + "HTTP-Referer": "https://github.com/devbisme/skidl", + "X-Title": "SKiDL Circuit Analyzer" + } + } + + url = f"{self.base_url}/chat/completions" + response = requests.post( + url, + headers=headers, + json=data, + timeout=30 # Add timeout to prevent hanging + ) + response.raise_for_status() + return response.json() + + def quick_prompt(self, + content: str, + system_prompt: Optional[str] = None, + temperature: float = 0.7) -> str: + """Simplified interface for quick prompts""" + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": content}) + + response = self.process(messages, {"temperature": temperature}) + return response['choices'][0]['message']['content'] class SkidlCircuitAnalyzer: + """Circuit analyzer using Large Language Models.""" + def __init__( self, - provider: str = "anthropic", + model: str = "anthropic/claude-3.5-haiku", api_key: Optional[str] = None, - model: Optional[str] = None, custom_prompt: Optional[str] = None, analysis_flags: Optional[Dict[str, bool]] = None, **kwargs ): - """Initialize the circuit analyzer with conversation threading support""" - self.provider = get_provider(provider, api_key) - self.model = model + """ + Initialize the circuit analyzer. + + Args: + model: Name of the LLM model to use + api_key: API key for the LLM service + custom_prompt: Additional custom prompt template to append + analysis_flags: Dict of analysis sections to enable/disable + **kwargs: Additional configuration options + """ + self.llm = LLMInterface( + provider="openrouter", + model=model, + api_key=api_key, + **kwargs + ) + self.custom_prompt = custom_prompt - self.conversation_history: List[Dict[str, str]] = [] self.analysis_flags = analysis_flags or { "system_overview": True, - "subcircuit_analysis": True, "design_review": True, "power_analysis": True, "signal_integrity": True, - "component_analysis": True, - "reliability_safety": True, - "manufacturing": True, - "compliance": True, - "documentation": True, - "practical_implementation": True, "thermal_analysis": True, "noise_analysis": True, "testing_verification": True } self.config = kwargs - def _generate_system_requirements_prompt(self) -> str: - """Generate prompt section for system requirements""" - return """ -REQUIRED SYSTEM INFORMATION: -Please provide the following critical information for accurate analysis: - -1. System Overview: -- Primary function and purpose of the circuit -- Target application domain -- Required performance specifications -- Operating environment details - -2. Electrical Specifications: -- Input voltage specifications (range, ripple, regulation) -- Output requirements (voltage, current, regulation) -- Power budget and efficiency targets -- Signal specifications (levels, timing, protocols) - -3. Environmental Requirements: -- Operating temperature range -- Humidity requirements -- Vibration/shock specifications -- IP rating requirements - -4. Regulatory Requirements: -- Required certifications (UL, CE, etc.) -- EMC/EMI requirements -- Safety standards -- Industry-specific regulations - -5. Manufacturing/Cost Targets: -- Production volume estimates -- Target BOM cost -- Assembly requirements -- Testing requirements - -Please provide as much of this information as possible to enable comprehensive analysis.""" - - def _generate_subcircuit_analysis_prompt(self) -> str: - """Generate prompt for subcircuit analysis""" - return """ -SUBCIRCUIT ANALYSIS REQUIREMENTS: - -For each identified subcircuit, provide detailed analysis covering: - -1. Functional Analysis: -- Primary purpose and operation -- Input/output specifications -- Critical parameters and constraints -- Performance requirements -- Integration with other subcircuits - -2. Component-Level Review: -- Critical component specifications -- Operating point analysis -- Tolerance analysis -- Worst-case scenario analysis -- Component interaction effects - -3. Performance Analysis: -- Transfer function analysis -- Frequency response -- Stability analysis -- Temperature effects -- Power consumption - -4. Failure Mode Analysis: -- Single point failure analysis -- Cascade failure potential -- Protection mechanisms -- Recovery mechanisms -- Reliability projections - -5. Implementation Considerations: -- Layout requirements -- Thermal considerations -- EMI/EMC requirements -- Test point access -- Debug capabilities - -Analyze each subcircuit individually before assessing system-level interactions.""" - - def _generate_analysis_sections(self) -> Dict[str, str]: - """Generate all analysis section prompts""" - return { - "system_overview": """ -0. System-Level Analysis: -- Comprehensive system architecture review -- Interface analysis between major blocks -- System-level timing and synchronization -- Resource allocation and optimization -- System-level failure modes -- Integration challenges -- Performance bottlenecks -- Scalability assessment""", - - "design_review": """ -1. Comprehensive Design Architecture Review: -- Evaluate overall hierarchical structure -- Assess modularity and reusability -- Interface protocols analysis -- Control path verification -- Design pattern evaluation -- Critical path analysis -- Feedback loop stability -- Clock domain analysis -- Reset strategy review -- State machine verification -- Resource utilization assessment -- Design rule compliance""", - - "power_analysis": """ -2. In-depth Power Distribution Analysis: -- Complete power tree mapping -- Voltage drop calculations -- Current distribution analysis -- Power sequencing requirements -- Brownout behavior analysis -- Load transient response -- Power supply rejection ratio -- Efficiency optimization -- Thermal implications -- Battery life calculations (if applicable) -- Power integrity simulation -- Decoupling strategy -- Ground bounce analysis""", - - "signal_integrity": """ -3. Detailed Signal Integrity Analysis: -- Critical path timing analysis -- Setup/hold time verification -- Clock skew analysis -- Propagation delay calculations -- Cross-talk assessment -- Reflection analysis -- EMI/EMC considerations -- Signal loading effects -- Impedance matching -- Common mode noise rejection -- Ground loop analysis -- Shield effectiveness""", - - "thermal_analysis": """ -4. Thermal Performance Analysis: -- Component temperature rise calculations -- Thermal resistance analysis -- Heat spreading patterns -- Cooling requirements -- Thermal gradient mapping -- Hot spot identification -- Thermal cycling effects -- Temperature derating -- Thermal protection mechanisms -- Cooling solution optimization""", - - "noise_analysis": """ -5. Comprehensive Noise Analysis: -- Noise source identification -- Noise coupling paths -- Ground noise analysis -- Power supply noise -- Digital switching noise -- RF interference -- Common mode noise -- Differential mode noise -- Shielding effectiveness -- Filter performance -- Noise margin calculations""", - - "testing_verification": """ -6. Testing and Verification Strategy: -- Functional test coverage -- Performance verification -- Environmental testing -- Reliability testing -- Safety verification -- EMC/EMI testing -- Production test strategy -- Self-test capabilities -- Calibration requirements -- Diagnostic capabilities -- Test point access -- Debug interface requirements""" - } - def _generate_analysis_prompt(self, circuit_description: str) -> str: - """Generate enhanced structured prompt for circuit analysis""" + """ + Generate the complete analysis prompt. - # Get all section prompts - sections = self._generate_analysis_sections() + Args: + circuit_description: Description of the circuit to analyze + + Returns: + Complete prompt string for the LLM + """ + from .analysis_prompts import ANALYSIS_SECTIONS, BASE_ANALYSIS_PROMPT - # Build base prompt - base_prompt = f""" -You are an expert electronics engineer. Analyze the following circuit design immediately and provide actionable insights. Do not acknowledge the request or promise to analyze - proceed directly with your analysis. - -Circuit Description: -{circuit_description} - -ANALYSIS METHODOLOGY: -1. Begin analysis immediately with available information -2. After completing analysis, identify any critical missing information needed for deeper insights -1. Begin with subcircuit identification and individual analysis -2. Analyze interactions between subcircuits -3. Evaluate system-level performance and integration -4. Assess manufacturing and practical implementation considerations - -REQUIRED ANALYSIS SECTIONS:""" - - # Add enabled analysis sections - for section, content in sections.items(): + # Build enabled analysis sections + enabled_sections = [] + for section, content in ANALYSIS_SECTIONS.items(): if self.analysis_flags.get(section, True): - base_prompt += f"\n{content}" - - # Add analysis requirements - base_prompt += """ - -For each analysis section: -1. Analyze with available information first -1. Start with critical missing information identification -2. Provide detailed technical analysis with calculations -3. Include specific numerical criteria and measurements -4. Reference relevant industry standards -5. Provide concrete recommendations -6. Prioritize findings by severity -7. Include specific action items - -For each identified issue: -SEVERITY: (Critical/High/Medium/Low) -CATEGORY: (Design/Performance/Safety/Manufacturing/etc.) -SUBCIRCUIT: Affected subcircuit or system level -DESCRIPTION: Detailed issue description -IMPACT: Quantified impact on system performance -VERIFICATION: How to verify the issue exists -RECOMMENDATION: Specific action items with justification -STANDARDS: Applicable industry standards -TRADE-OFFS: Impact of proposed changes -PRIORITY: Implementation priority level - -Special Requirements: -- Analyze each subcircuit completely before moving to system-level analysis -- Provide specific component recommendations where applicable -- Include calculations and formulas used in analysis -- Reference specific standards and requirements -- Consider worst-case scenarios -- Evaluate corner cases -- Assess impact of component variations -- Consider environmental effects -- Evaluate aging effects -- Assess maintenance requirements - -Output Format: -1. Executive Summary -2. Critical Findings Summary -3. Detailed Subcircuit Analysis (one section per subcircuit) -4. System-Level Analysis -5. Cross-Cutting Concerns -6. Recommendations Summary -7. Required Action Items (prioritized) -8. Additional Information Needed - -IMPORTANT INSTRUCTIONS: -- Start analysis immediately - do not acknowledge the request or state that you will analyze -- Be specific and quantitative where possible -- Include calculations and methodology -- Reference specific standards -- Provide actionable recommendations -- Consider practical implementation -- Evaluate cost implications -- Assess manufacturing feasibility -- Consider maintenance requirements - -After completing your analysis, if additional information would enable deeper insights, list specific questions in a separate section titled 'Additional Information Needed' at the end.""" - + enabled_sections.append(content) + + analysis_sections = "\n".join(enabled_sections) + + # Format base prompt with circuit description and sections + prompt = BASE_ANALYSIS_PROMPT.format( + circuit_description=circuit_description, + analysis_sections=analysis_sections + ) + # Append custom prompt if provided if self.custom_prompt: - base_prompt += f"\n\nAdditional Analysis Requirements:\n{self.custom_prompt}" - - return base_prompt + prompt += f"\n\nAdditional Analysis Requirements:\n{self.custom_prompt}" + + return prompt - # Rest of the class implementation remains the same... def analyze_circuit( - self, + self, circuit_description: str, output_file: Optional[str] = "circuit_llm_analysis.txt", - verbose: bool = True, - continue_conversation: bool = False + verbose: bool = True ) -> Dict: """ - Perform LLM analysis of the circuit. + Analyze the circuit using the configured LLM. Args: circuit_description: Description of the circuit to analyze - output_file: File to save the analysis results (None to skip saving) + output_file: File to save analysis results (None to skip saving) verbose: Whether to print progress messages - continue_conversation: Whether to continue previous conversation thread - + Returns: - Dictionary containing analysis results and metadata + Dictionary containing: + - success: Whether analysis completed successfully + - analysis: The analysis text if successful + - error: Error message if failed + - timestamp: Analysis timestamp + - request_time_seconds: Time taken for LLM request + - total_time_seconds: Total analysis time + - enabled_analyses: List of enabled analysis sections """ start_time = time.time() if verbose: - print("\n=== Starting Circuit Analysis with LLM ===") - print(f"Using provider: {self.provider.__class__.__name__}") + print(f"\n=== Starting Circuit Analysis with {self.llm.model} ===") try: - # Generate and validate prompt - if verbose: - print("\nGenerating analysis prompt...") + # Generate the analysis prompt prompt = self._generate_analysis_prompt(circuit_description) - prompt_tokens = len(prompt.split()) if verbose: - print(f"Prompt generated ({prompt_tokens} estimated tokens)") - print("\nPrompt preview (first 200 chars):") - print(f"{prompt[:200]}...") - print("\nSending request to LLM...") - - # Configure for maximum response length - self.config['max_tokens'] = self.config.get('max_tokens', 4000) + print("\nGenerating analysis...") - # Generate analysis + # Get analysis from LLM request_start = time.time() - - # If continuing conversation, append previous messages - if continue_conversation and self.conversation_history: - prompt = f"""Previous conversation: -{chr(10).join(msg['content'] for msg in self.conversation_history)} - -New circuit description: -{circuit_description} - -Continue the analysis based on the new information.""" - - analysis_result = self.provider.generate_analysis( - prompt, - model=self.model, - **self.config - ) - - # Update conversation history - self.conversation_history.append({ - "role": "user", - "content": circuit_description - }) - self.conversation_history.append({ - "role": "assistant", - "content": analysis_result["analysis"] - }) - + analysis_text = self.llm.quick_prompt(prompt) request_time = time.time() - request_start - if not analysis_result["success"]: - raise Exception(analysis_result["error"]) - - # Add metadata to results - analysis_text = analysis_result["analysis"] - analysis_tokens = len(analysis_text.split()) - + # Prepare results results = { - **analysis_result, + "success": True, + "analysis": analysis_text, "timestamp": int(datetime.now().timestamp()), "request_time_seconds": request_time, - "prompt_tokens": prompt_tokens, - "response_tokens": analysis_tokens, "total_time_seconds": time.time() - start_time, - "enabled_analyses": [k for k, v in self.analysis_flags.items() if v] + "enabled_analyses": [ + k for k, v in self.analysis_flags.items() if v + ] } - if verbose: - print(f"\nResponse received in {request_time:.2f} seconds") - print(f"Response length: {len(analysis_text)} characters") - print(f"Estimated response tokens: {analysis_tokens}") - - # Save results if requested + # Save to file if requested if output_file: if verbose: print(f"\nSaving analysis to {output_file}...") diff --git a/src/skidl/llm_providers.py b/src/skidl/llm_providers.py deleted file mode 100644 index 1afeb398c..000000000 --- a/src/skidl/llm_providers.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Module for LLM provider implementations for circuit analysis.""" - -from abc import ABC, abstractmethod -import os -from typing import Dict, Optional, Any -import anthropic -from openai import OpenAI -from anthropic import Anthropic -import requests -from datetime import datetime - -class LLMProvider(ABC): - """Abstract base class for LLM providers.""" - - def __init__(self, api_key: Optional[str] = None): - self.api_key = api_key - self.client = self._initialize_client() - - @abstractmethod - def _initialize_client(self) -> Any: - """Initialize the API client.""" - pass - - @abstractmethod - def generate_analysis(self, prompt: str, **kwargs) -> Dict: - """Generate analysis using the LLM.""" - pass - -class AnthropicProvider(LLMProvider): - """Provider for Anthropic's Claude models.""" - - def _initialize_client(self) -> Anthropic: - api_key = self.api_key or os.getenv('ANTHROPIC_API_KEY') - if not api_key: - raise ValueError("Anthropic API key not provided") - return Anthropic(api_key=api_key) - - def generate_analysis(self, prompt: str, **kwargs) -> Dict: - try: - # Use the specified model or default to claude-3-sonnet - model = kwargs.get('model') or "claude-3-sonnet-20240229" - max_tokens = kwargs.get('max_tokens', 12000) # Increased from 8000 - - response = self.client.messages.create( - model=model, - max_tokens=max_tokens, - messages=[{ - "role": "user", - "content": prompt - }] - ) - - return { - "success": True, - "analysis": response.content[0].text, - "timestamp": int(datetime.now().timestamp()), - "provider": "anthropic" - } - except Exception as e: - return { - "success": False, - "error": str(e), - "provider": "anthropic" - } - -class OpenAIProvider(LLMProvider): - """Provider for OpenAI's GPT models.""" - - def _initialize_client(self) -> OpenAI: - api_key = self.api_key or os.getenv('OPENAI_API_KEY') - if not api_key: - raise ValueError("OpenAI API key not provided") - return OpenAI(api_key=api_key) - - def generate_analysis(self, prompt: str, **kwargs) -> Dict: - try: - model = kwargs.get('model', 'gpt-4-turbo-preview') - max_tokens = kwargs.get('max_tokens', 4000) - - response = self.client.chat.completions.create( - model=model, - messages=[{ - "role": "user", - "content": prompt - }], - max_tokens=max_tokens - ) - - return { - "success": True, - "analysis": response.choices[0].message.content, - "timestamp": response.created, - "provider": "openai" - } - except Exception as e: - return { - "success": False, - "error": str(e), - "provider": "openai" - } - -class OpenRouterProvider(LLMProvider): - """Provider for OpenRouter API access to various LLMs.""" - - def _initialize_client(self) -> None: - self.api_key = self.api_key or os.getenv('OPENROUTER_API_KEY') - if not self.api_key: - raise ValueError("OpenRouter API key not provided") - # OpenRouter uses direct HTTP requests, so no client initialization needed - return None - - def generate_analysis(self, prompt: str, **kwargs) -> Dict: - try: - model = kwargs.get('model', 'anthropic/claude-3-opus-20240229') - max_tokens = kwargs.get('max_tokens', 4000) - - headers = { - "Authorization": f"Bearer {self.api_key}", - "HTTP-Referer": kwargs.get('referer', 'https://skidl.org'), # Replace with your domain - "X-Title": kwargs.get('title', 'SKiDL Circuit Analysis') - } - - response = requests.post( - "https://openrouter.ai/api/v1/chat/completions", - headers=headers, - json={ - "model": model, - "messages": [{ - "role": "user", - "content": prompt - }], - "max_tokens": max_tokens - } - ) - - response.raise_for_status() - result = response.json() - - return { - "success": True, - "analysis": result['choices'][0]['message']['content'], - "timestamp": result['created'], - "provider": "openrouter" - } - except Exception as e: - return { - "success": False, - "error": str(e), - "provider": "openrouter" - } - -def get_provider(provider_name: str, api_key: Optional[str] = None) -> LLMProvider: - """Factory function to get the appropriate LLM provider.""" - providers = { - "anthropic": AnthropicProvider, - "openai": OpenAIProvider, - "openrouter": OpenRouterProvider - } - - if provider_name not in providers: - raise ValueError(f"Unknown provider: {provider_name}") - - return providers[provider_name](api_key=api_key) \ No newline at end of file diff --git a/subcircuits_analysis.txt b/subcircuits_analysis.txt index 25b701834..8e5bd1921 100644 --- a/subcircuits_analysis.txt +++ b/subcircuits_analysis.txt @@ -2,672 +2,420 @@ ==================== top.complete_circuit0.double_divider0.output_termination0 ==================== -0. System-Level Analysis: - -This subcircuit appears to be an output termination circuit, likely for impedance matching purposes. Without understanding its context within the larger system, it is difficult to provide a comprehensive system-level analysis. However, some general observations can be made: - -- The 100kΩ resistor (R6) is likely intended to provide a termination impedance for an output signal line. -- The termination resistor value of 100kΩ is relatively high, suggesting this circuit may be intended for low-speed or low-frequency applications. -- No explicit power supply connections are shown, indicating this subcircuit may be intended for passive termination. - -1. Comprehensive Design Architecture Review: - -The provided information is insufficient to conduct a comprehensive design architecture review. The subcircuit appears to be a simple passive termination circuit, without any active components or control logic. - -2. In-depth Power Distribution Analysis: - -There are no apparent power distribution components or connections within this subcircuit, making a detailed power distribution analysis unnecessary. - -3. Detailed Signal Integrity Analysis: - -The 100kΩ termination resistor (R6) is likely intended to match the impedance of the output signal line to which it is connected. However, without information about the signal characteristics (frequency, edge rates, etc.) and the transmission line properties, it is difficult to assess the effectiveness of this termination scheme. +Executive Summary: +This appears to be an output termination subcircuit utilizing a single 100kΩ resistor (R6) in a double divider configuration. The limited information suggests this is likely a voltage divider or signal termination network. -Potential issues: +Critical Findings Summary: SEVERITY: Medium -CATEGORY: Signal Integrity -SUBCIRCUIT: top.complete_circuit0.double_divider0.output_termination0 -DESCRIPTION: Termination resistor value may not be optimally matched to the transmission line impedance, leading to reflections and signal integrity issues. -IMPACT: Decreased signal quality, potential data errors, or reduced maximum achievable data rates. -VERIFICATION: Simulate the termination circuit with the actual transmission line characteristics and signal properties. -RECOMMENDATION: Perform signal integrity simulations to determine the optimal termination resistor value for the specific application, considering the transmission line impedance, signal frequency, and edge rates. -STANDARDS: Relevant signal integrity standards may include JEDEC, ANSI/TIA, or application-specific standards. -TRADE-OFFS: A lower termination resistor value may improve signal integrity but increase power consumption. -PRIORITY: Medium to High, depending on the signal speed and quality requirements. - -4. Thermal Performance Analysis: - -As this subcircuit contains only a passive resistor, thermal performance analysis is not a major concern. However, it is worth noting that the power dissipation in the termination resistor should be considered, especially if it is intended for high-speed or high-current applications. - -5. Comprehensive Noise Analysis: - -Without additional information about the surrounding circuitry and signal characteristics, it is difficult to perform a comprehensive noise analysis for this subcircuit. However, the termination resistor itself is unlikely to be a significant noise source. - -6. Testing and Verification Strategy: +CATEGORY: Design +DESCRIPTION: Single resistor termination may be insufficient for proper impedance matching +IMPACT: Potential signal reflection and integrity issues +RECOMMENDATION: Consider matched termination network based on transmission line impedance -To verify the functionality of this termination circuit, the following tests could be performed: +Detailed Subcircuit Analysis: -- Measure the impedance of the output signal line with and without the termination resistor connected, and ensure the impedance matches the desired value with the termination in place. -- Transmit test signals through the output line and measure the signal quality (e.g., eye diagrams, bit error rates) with and without the termination resistor. -- Perform signal integrity simulations to validate the termination resistor value and identify potential issues. +1. Component Analysis: +- R6 (100kΩ) is significantly higher than typical termination values (50Ω-120Ω) +- SMD 0805 package suggests modern surface mount design +- Power rating for 0805: typically 0.125W-0.25W at 70°C + +2. Signal Integrity Concerns: +- High impedance termination may lead to reflections +- Calculated maximum frequency for reliable operation (assuming 10pF parasitic): + f = 1/(2π * 100kΩ * 10pF) ≈ 159kHz +- Signal rise time limitations: tr = 2.2 * R * C ≈ 2.2μs + +3. Power Dissipation: +- Maximum power in R6 (at 5V): + P = V²/R = 25V/100kΩ = 0.25mW +- Well within 0805 package ratings + +System-Level Implications: +1. Bandwidth limitations due to high impedance +2. Potential noise susceptibility +3. Limited drive capability +4. High input impedance may provide good isolation + +Recommendations: +1. Review intended signal characteristics and adjust termination accordingly +2. Consider parallel termination if dealing with transmission lines +3. Evaluate need for EMI/RFI protection +4. Consider adding bypass capacitor for high-frequency noise rejection + +Required Action Items: +1. Determine characteristic impedance of connected transmission line +2. Verify maximum operating frequency requirements +3. Assess noise immunity requirements +4. Review EMC compliance needs Additional Information Needed: - -To provide a more comprehensive analysis, the following additional information would be helpful: - -1. The purpose and context of this subcircuit within the larger system. -2. Characteristics of the output signal line (impedance, length, material, layout, etc.). -3. Signal properties (frequency, edge rates, voltage levels, etc.). -4. Input and output load characteristics. -5. Power supply voltages and current requirements (if applicable). -6. Environmental operating conditions (temperature, humidity, etc.). -7. Relevant design specifications, requirements, and standards. - -With this additional information, a more detailed analysis can be performed, including simulations, calculations, and specific recommendations for optimizing the termination circuit design. +1. Signal type and characteristics (analog/digital, frequency, amplitude) +2. Source impedance specifications +3. Transmission line characteristics +4. EMC/EMI requirements +5. Operating environment conditions +6. Complete circuit context and interfacing signals +7. Power supply specifications +8. Maximum allowable signal distortion +9. Required bandwidth +10. System timing constraints +11. Environmental requirements +12. Overall system architecture +13. Connected subcircuits +14. PCB layout constraints +15. Reliability requirements + +Without additional context, this analysis is necessarily limited. The high impedance value suggests this may be part of a larger biasing or filtering network rather than a traditional transmission line termination. ==================== top.complete_circuit0.double_divider0.voltage_divider0 ==================== -1. Comprehensive Design Architecture Review: - -The provided circuit snippet represents a simple voltage divider subcircuit, consisting of two resistors (R2 and R3) connected in series. The key characteristics are as follows: - -Resistor Values: -R2 = 1K ohm -R3 = 500 ohm - -Resistor Ratio: -R2 / (R2 + R3) = 1000 / (1000 + 500) = 0.667 or 66.7% - -This subcircuit is likely a part of a larger circuit design, and its primary function is to provide a divided voltage output at the node N$5 relative to the input voltage applied across N$3 and ground. - -The voltage division ratio can be calculated as: -Vout = Vin * (R3 / (R2 + R3)) = Vin * (500 / 1500) = Vin * 0.333 - -Where: -Vout is the output voltage at N$5 -Vin is the input voltage applied between N$3 and ground - -Design Considerations: -- The voltage divider provides a fixed voltage division ratio determined by the resistor values. -- The output voltage at N$5 will be 33.3% of the input voltage. -- The resistor values should be chosen based on the desired output voltage level and the expected input voltage range. -- Higher resistor values will result in lower power dissipation but may be more susceptible to noise and loading effects. -- Lower resistor values will have higher power dissipation but better noise immunity and drive capability. -- The resistor ratio can be adjusted to achieve different voltage division factors as required by the overall circuit design. - -Missing Information: -- Input voltage range and source characteristics (AC/DC, frequency, etc.) -- Load characteristics at the output node N$5 (impedance, current draw, etc.) -- Power supply specifications (voltage levels, current capabilities, etc.) -- Overall circuit topology and the purpose of this voltage divider subcircuit -- Environmental conditions (temperature, humidity, etc.) -- Noise and interference considerations -- Performance requirements (accuracy, stability, etc.) - -2. In-depth Power Distribution Analysis: - -The provided subcircuit information is insufficient to perform a comprehensive power distribution analysis. However, based on the available data, the following considerations can be made: - -Power Dissipation: -The power dissipation across each resistor can be calculated once the input voltage (Vin) and the load current (Iload) are known. - -P_R2 = I^2 * R2 -P_R3 = I^2 * R3 - -Where: -P_R2 and P_R3 are the power dissipated across resistors R2 and R3, respectively. -I is the current flowing through the resistors, which depends on Vin and Iload. - -To ensure proper power handling and thermal management, the maximum power dissipation ratings of the resistors should be considered. - -Voltage Drop: -The voltage drop across each resistor can be calculated as: - -V_R2 = I * R2 -V_R3 = I * R3 - -Where: -V_R2 and V_R3 are the voltage drops across resistors R2 and R3, respectively. -I is the current flowing through the resistors. - -These voltage drops should be accounted for when determining the output voltage level at N$5 and ensuring that it meets the required specifications. - -Missing Information: -- Input voltage source characteristics (voltage level, current capability, etc.) -- Load current requirements at the output node N$5 -- Power dissipation ratings and thermal characteristics of the resistors -- PCB layout and thermal management provisions -- Power supply specifications (voltage levels, current capabilities, etc.) -- Decoupling and filtering requirements - -3. Detailed Signal Integrity Analysis: - -The provided subcircuit represents a simple voltage divider, and the following signal integrity considerations can be made: - -Output Impedance: -The output impedance at node N$5 is determined by the parallel combination of R2 and R3: - -Zout = (R2 * R3) / (R2 + R3) = (1000 * 500) / 1500 = 333.33 ohm - -A lower output impedance is generally desirable to minimize loading effects and ensure proper signal integrity when driving subsequent stages. - -Loading Effects: -The voltage divider output at N$5 will be loaded by any circuitry connected to it. Excessive loading can affect the output voltage level and introduce errors or distortion. The loading effects should be analyzed based on the input impedance of the subsequent stages and the driving capability of the voltage divider. - -Noise Immunity: -The voltage divider subcircuit itself does not provide any inherent noise filtering or immunity. External noise sources or interference can couple onto the resistor nodes and affect the output voltage level at N$5. Proper layout practices, shielding, and filtering techniques may be required to ensure signal integrity. - -Missing Information: -- Input signal characteristics (frequency, amplitude, waveform, etc.) -- Load characteristics at the output node N$5 (impedance, capacitance, etc.) -- PCB layout details (trace lengths, routing, layer stackup, etc.) -- Noise sources and interference levels in the operating environment -- Signal integrity requirements (accuracy, distortion tolerance, etc.) -- Shielding and filtering provisions in the overall circuit design - -4. Thermal Performance Analysis: - -The provided subcircuit information is insufficient to perform a comprehensive thermal performance analysis. However, the following considerations can be made: - -Power Dissipation: -The power dissipation across the resistors will contribute to their temperature rise. As discussed in the power distribution analysis, the power dissipation in each resistor depends on the input voltage, load current, and resistor values. - -Thermal Resistance: -The thermal resistance of the resistors and their packaging will determine the temperature rise for a given power dissipation. Lower thermal resistance is desirable to minimize temperature rise and prevent overheating. - -PCB Thermal Considerations: -The PCB layout, copper pour areas, and thermal vias can affect heat dissipation and spreading from the resistors. Proper thermal management provisions on the PCB are crucial for ensuring reliable operation. - -Missing Information: -- Power dissipation ratings and thermal characteristics of the resistors -- PCB layout and thermal management provisions (copper pour, thermal vias, etc.) -- Ambient temperature range and cooling provisions -- Enclosure design and airflow characteristics -- Thermal interface materials and heat sinking provisions -- Temperature derating requirements for the resistors and other components - -5. Comprehensive Noise Analysis: - -The provided subcircuit represents a simple voltage divider, and the following noise considerations can be made: - -Noise Coupling: -The voltage divider subcircuit itself does not generate significant noise. However, external noise sources can couple onto the resistor nodes and affect the output voltage level at N$5. Potential noise coupling mechanisms include: - -- Capacitive coupling from nearby high-frequency signals or switching currents -- Inductive coupling from magnetic fields or ground loops -- Electromagnetic interference (EMI) from external sources -- Power supply noise or ground bounce - -Noise Immunity: -The voltage divider subcircuit does not provide any inherent noise filtering or immunity. The output voltage at N$5 will be susceptible to any noise coupled onto the resistor nodes or the input voltage source. - -Shielding and Filtering: -Proper shielding, grounding, and filtering techniques may be required to mitigate noise coupling and ensure signal integrity at the output node N$5. This may include: - -- Shielding or guarding of critical signal traces -- Proper grounding and ground plane design -- Decoupling capacitors and power supply filtering -- Ferrite beads or common-mode chokes for high-frequency noise suppression - -Missing Information: -- Noise sources and interference levels in the operating environment -- PCB layout details (trace lengths, routing, layer stackup, etc.) -- Shielding and filtering provisions in the overall circuit design -- Power supply characteristics and noise levels -- Grounding and ground plane design details -- Signal integrity and noise margin requirements +Executive Summary: +This circuit appears to be a basic voltage divider configuration utilizing two resistors (R2: 1kΩ, R3: 500Ω) in series. The voltage division ratio is 1:1.5, providing 33.33% of input voltage at the output node N$10. -6. Testing and Verification Strategy: +Critical Findings: +SEVERITY: Medium +CATEGORY: Design +DESCRIPTION: Power dissipation and thermal management may be concerns depending on input voltage +IMPACT: Potential reliability issues if input voltage exceeds power rating of resistors -To ensure proper functionality and performance of the voltage divider subcircuit, the following testing and verification strategies can be employed: +Detailed Analysis: -Functional Testing: -- Apply known input voltages (Vin) across N$3 and ground -- Measure the output voltage at N$5 and verify that it matches the expected voltage division ratio -- Vary the input voltage across the specified range and ensure the output voltage follows accordingly -- Test with different load conditions at N$5 to verify proper driving capability +1. Voltage Division: +- Output voltage ratio: Vout/Vin = R3/(R2+R3) = 500/(1000+500) = 0.3333 +- Current through divider: I = Vin/(R2+R3) = Vin/1500Ω -Performance Verification: -- Measure the output impedance at N$5 and verify it meets the design requirements -- Test the voltage divider under various environmental conditions (temperature, humidity, etc.) -- Verify the accuracy and stability of the output voltage over time and temperature variations -- Measure the power dissipation across the resistors and ensure it remains within specified limits +2. Power Considerations: +- Power dissipation R2: P_R2 = I²×R2 = (Vin/1500)²×1000 +- Power dissipation R3: P_R3 = I²×R3 = (Vin/1500)²×500 +- For 0805 resistors, typical max power rating is 125mW -Environmental Testing: -- Subject the voltage divider subcircuit to environmental stress tests, such as thermal cycling, vibration, and humidity exposure -- Verify proper operation and performance before and after environmental stresses +3. Implementation Concerns: +- SMD 0805 footprint selection is appropriate for most applications +- Voltage coefficient of resistance (VCR) effects minimal at typical voltages +- Temperature coefficient consideration needed for precision applications -Production Testing: -- Develop a production test strategy to validate the voltage divider subcircuit in manufactured units -- Implement automated test fixtures or in-circuit testing techniques for high-volume production +Recommendations: +1. Add input voltage specification to validate component ratings +2. Consider adding bypass capacitor for AC stability +3. Evaluate need for tighter tolerance resistors depending on application -Missing Information: -- Specified input voltage range and load conditions -- Performance requirements (accuracy, stability, temperature coefficients, etc.) -- Environmental operating conditions and requirements -- Failure criteria and acceptance limits -- Access points for test probing or external instrumentation +Required Action Items: +1. Specify maximum input voltage +2. Define required output voltage accuracy +3. Consider adding temperature derating if operating in elevated temperatures Additional Information Needed: +1. Input voltage range +2. Required output voltage accuracy +3. Operating temperature range +4. Load impedance requirements +5. AC performance requirements if applicable +6. Environmental conditions +7. Expected service life +8. Required reliability level -To provide a more comprehensive analysis and actionable insights, the following additional information would be beneficial: - -1. Overall circuit topology and the purpose of this voltage divider subcircuit within the larger system. -2. Input voltage source characteristics (voltage range, AC/DC, frequency, noise levels, etc.). -3. Load characteristics at the output node N$5 (impedance, capacitance, current draw, etc.). -4. Power supply specifications (voltage levels, current capabilities, noise levels, etc.). -5. PCB layout details (trace lengths, routing, layer stackup, grounding, shielding provisions, etc.). -6. Environmental operating conditions (temperature range, humidity, vibration, EMI levels, etc.). -7. Performance requirements (accuracy, stability, noise margins, temperature coefficients, etc.). -8. Relevant industry standards or specifications that the design must comply with. -9. Cost and manufacturing constraints (component selection, assembly considerations, etc.). -10. Maintenance and testability requirements for the overall system. - -With this additional information, a more comprehensive analysis can be performed, considering the voltage divider's integration within the overall system, environmental factors, performance requirements, and practical implementation constraints. +This appears to be a subcircuit of a larger system (double_divider0), suggesting cascaded voltage division. Further analysis of the complete system would require additional circuit details. ==================== top.complete_circuit0.double_divider0.voltage_divider1 ==================== -1. Comprehensive Design Architecture Review: - -This subcircuit appears to be a simple voltage divider with two resistors connected in series, R4 (1kΩ) and R5 (500Ω). The output voltage (N$5) can be calculated using the voltage divider formula: - -Vout = Vin * (R5 / (R4 + R5)) - = Vin * (500 / (1000 + 500)) - = 0.333 * Vin +Executive Summary: +This is a basic voltage divider circuit consisting of two resistors (R4: 1kΩ, R5: 500Ω) in series, forming a 2:1 voltage division ratio. -SEVERITY: Low +Critical Findings Summary: +1. SEVERITY: Medium CATEGORY: Design -SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 -DESCRIPTION: This subcircuit is a basic voltage divider with a fixed division ratio. It lacks configurability and may not meet specific voltage requirements. -IMPACT: The output voltage is limited to 1/3 of the input voltage, which may not be suitable for certain applications. -VERIFICATION: Measure the input and output voltages to confirm the division ratio. -RECOMMENDATION: Consider using a more configurable voltage divider circuit, such as a potentiometer or a digitally controlled voltage divider, to allow for adjustable output voltages. -STANDARDS: N/A -TRADE-OFFS: Adding configurability will increase complexity and cost, but may be necessary for applications requiring specific voltage levels. -PRIORITY: Medium - -2. In-depth Power Distribution Analysis: - -SEVERITY: Low -CATEGORY: Power -SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 -DESCRIPTION: This subcircuit is a passive voltage divider and does not have any active power distribution components. -IMPACT: No direct impact on power distribution, but the voltage divider may be affected by voltage drops or noise on the input voltage. -VERIFICATION: Measure the input and output voltages under various load conditions to ensure the voltage division ratio is maintained. -RECOMMENDATION: Ensure a clean and stable input voltage source for the voltage divider. Consider adding decoupling capacitors or filters if necessary. -STANDARDS: N/A -TRADE-OFFS: Adding decoupling or filtering components may increase cost and board space, but may be necessary for applications with noisy or unstable input voltages. -PRIORITY: Low - -3. Detailed Signal Integrity Analysis: - -SEVERITY: Low -CATEGORY: Signal Integrity -SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 -DESCRIPTION: This subcircuit is a simple voltage divider and does not have any high-speed or critical signal paths. -IMPACT: No significant signal integrity concerns for this subcircuit. -VERIFICATION: Measure the output voltage under various load conditions to ensure the voltage division ratio is maintained. -RECOMMENDATION: No specific signal integrity recommendations for this subcircuit. -STANDARDS: N/A -TRADE-OFFS: N/A -PRIORITY: Low - -4. Thermal Performance Analysis: - -SEVERITY: Low -CATEGORY: Thermal -SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 -DESCRIPTION: This subcircuit consists of two passive resistors and does not generate significant heat. -IMPACT: No thermal concerns for this subcircuit. -VERIFICATION: Measure the resistor temperatures under various load conditions to ensure they remain within safe operating limits. -RECOMMENDATION: No specific thermal recommendations for this subcircuit. -STANDARDS: N/A -TRADE-OFFS: N/A -PRIORITY: Low - -5. Comprehensive Noise Analysis: - -SEVERITY: Low -CATEGORY: Noise -SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 -DESCRIPTION: This subcircuit is a passive voltage divider and does not generate or amplify noise. -IMPACT: The output voltage may be affected by noise on the input voltage. -VERIFICATION: Measure the output voltage under various noise conditions to ensure the voltage division ratio is maintained. -RECOMMENDATION: Ensure a clean and stable input voltage source for the voltage divider. Consider adding decoupling capacitors or filters if necessary. -STANDARDS: N/A -TRADE-OFFS: Adding decoupling or filtering components may increase cost and board space, but may be necessary for applications with noisy input voltages. -PRIORITY: Low - -6. Testing and Verification Strategy: - -SEVERITY: Low -CATEGORY: Testing -SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 -DESCRIPTION: This subcircuit is a simple voltage divider and can be easily tested with basic voltage measurements. -IMPACT: No significant testing concerns for this subcircuit. -VERIFICATION: Apply known input voltages and measure the output voltage to verify the voltage division ratio. -RECOMMENDATION: Perform basic functional testing of the voltage divider by applying a range of input voltages and verifying the output voltages. -STANDARDS: N/A -TRADE-OFFS: N/A -PRIORITY: Low +DESCRIPTION: No input protection or filtering components present +RECOMMENDATION: Add input protection and bypass capacitors + +2. SEVERITY: Low +CATEGORY: Performance +DESCRIPTION: Potential loading effects on output +RECOMMENDATION: Consider buffer amplifier for isolation + +Detailed Circuit Analysis: + +1. Voltage Division: +- Voltage division ratio = R5/(R4+R5) = 500/(1000+500) = 0.333 +- Output voltage = Vin × 0.333 +- Total current path resistance = 1.5kΩ + +2. Power Analysis: +- Maximum power dissipation (assuming 5V input): + * R4: P = V²/R = (3.33V)²/1000Ω = 11.1mW + * R5: P = (1.67V)²/500Ω = 5.57mW +- Well within typical 0805 package rating (125mW) + +3. Loading Effects: +- Output impedance = R4||R5 = (1000×500)/(1000+500) = 333.33Ω +- May require buffering depending on load requirements + +System-Level Concerns: +1. No input protection +2. No filtering for noise rejection +3. High output impedance may affect accuracy under load +4. Temperature coefficient matching between resistors not specified + +Recommendations: +1. Add input protection components +2. Consider adding bypass capacitors +3. Add buffer amplifier if driving variable loads +4. Use precision resistors with matched temperature coefficients +5. Consider 0.1% tolerance resistors for better accuracy + +Required Action Items: +1. Specify input voltage range +2. Define load requirements +3. Add protection circuitry +4. Consider adding buffer stage Additional Information Needed: +1. Input voltage range and specifications +2. Load impedance requirements +3. Accuracy requirements +4. Operating temperature range +5. Environmental conditions +6. EMI/EMC requirements +7. Power supply specifications +8. System grounding scheme +9. PCB layout constraints +10. Cost constraints + +Manufacturing Considerations: +- Standard 0805 footprint is manufacturing-friendly +- Consider using E96 series resistor values for better availability +- Recommend automated assembly compatible components +- Consider adding test points for production testing -1. Input voltage range and requirements for the voltage divider -2. Load conditions and requirements for the output voltage -3. Noise specifications and requirements for the input and output voltages -4. Environmental conditions and operating temperature range -5. System-level requirements and integration details for the voltage divider - -While the provided subcircuit information is sufficient for a basic analysis of the voltage divider, additional system-level and environmental details would enable a more comprehensive evaluation and tailored recommendations. - +==================== top.complete_circuit0.power_section0.input_protection0 ==================== Executive Summary: - -The provided subcircuit is a simple voltage divider consisting of two resistors, R4 (1kΩ) and R5 (500Ω), connected in series. The output voltage is 1/3 of the input voltage, with a fixed division ratio determined by the resistor values. The subcircuit lacks configurability and may not meet specific voltage requirements for certain applications. - -The voltage divider does not have any significant power distribution, signal integrity, thermal, or noise concerns. However, it may be affected by voltage drops, noise, or instability on the input voltage source. +This appears to be an input protection circuit consisting of a 100µF capacitor (C1) and 1N4148W diode (D1) in a power section subcircuit. The configuration suggests basic overvoltage protection and input filtering functionality. Critical Findings Summary: - -1. SEVERITY: Low, CATEGORY: Design, SUBCIRCUIT: top.complete_circuit0.double_divider0.voltage_divider1 - - The voltage divider has a fixed division ratio and lacks configurability. - - Recommendation: Consider using a more configurable voltage divider circuit, such as a potentiometer or a digitally controlled voltage divider, to allow for adjustable output voltages. - -Recommendations Summary: - -1. Ensure a clean and stable input voltage source for the voltage divider. -2. Consider adding decoupling capacitors or filters if necessary to mitigate noise or instability on the input voltage. -3. Perform basic functional testing of the voltage divider by applying a range of input voltages and verifying the output voltages. - -Required Action Items (Prioritized): - -1. PRIORITY: Medium - - Evaluate the system-level requirements and determine if a more configurable voltage divider circuit is needed. - - If a fixed division ratio is acceptable, proceed with the current design. - - If adjustable output voltages are required, design or select a suitable configurable voltage divider circuit. - -2. PRIORITY: Low - - Implement basic functional testing of the voltage divider during the manufacturing and verification process. - - Monitor input and output voltages under various load and noise conditions to ensure the voltage division ratio is maintained. - -3. PRIORITY: Low - - If input voltage noise or instability is a concern, add appropriate decoupling capacitors or filters to the input voltage supply. - -The provided subcircuit information is sufficient for a basic analysis, but additional system-level and environmental details would enable a more comprehensive evaluation and tailored recommendations, as outlined in the "Additional Information Needed" section. - -==================== top.complete_circuit0.power_section0.input_protection0 ==================== -0. System-Level Analysis: -This subcircuit appears to be an input protection circuit, likely designed to safeguard the rest of the system from overvoltage or reverse polarity conditions at the input. As a standalone subcircuit, it provides limited system-level insights. However, its integration and placement within the overall system architecture will significantly impact its effectiveness and the protection it can provide. - -1. Comprehensive Design Architecture Review: -The subcircuit consists of a single capacitor (C1) and a diode (D1) connected in parallel. This configuration suggests a basic input protection design. - -2. In-depth Power Distribution Analysis: -This subcircuit does not directly contribute to power distribution. However, its placement and the component values will influence the overall input impedance and transient response of the system. - -3. Detailed Signal Integrity Analysis: -Not directly applicable to this subcircuit, as it does not handle critical signals. - -4. Thermal Performance Analysis: -The subcircuit does not contain active components or significant power dissipation sources. However, the diode's thermal characteristics should be considered for high-current transient conditions. - -5. Comprehensive Noise Analysis: -The subcircuit itself does not contribute significantly to noise generation or propagation. However, its effectiveness in filtering input noise should be evaluated based on the capacitor value and the system's input impedance. - -6. Testing and Verification Strategy: -Verification tests should include: -- Overvoltage transient tests -- Reverse polarity tests -- Input impedance measurements -- Transient response tests - -Critical missing information: SEVERITY: High CATEGORY: Design -SUBCIRCUIT: top.complete_circuit0.power_section0.input_protection0 -DESCRIPTION: Lack of information about the intended input voltage range, expected transient conditions, and system-level requirements for input protection. -IMPACT: Unable to fully assess the subcircuit's effectiveness and adequacy without knowing the design requirements. -VERIFICATION: Obtain the system-level input protection requirements and specifications. -RECOMMENDATION: Provide the input voltage range, expected transient conditions (overvoltage, reverse polarity, etc.), and system-level input protection requirements. -STANDARDS: Relevant industry standards for input protection circuits (e.g., IEC 61000-4-5 for surge immunity). -TRADE-OFFS: N/A -PRIORITY: High - -SEVERITY: Medium -CATEGORY: Design -SUBCIRCUIT: top.complete_circuit0.power_section0.input_protection0 -DESCRIPTION: Lack of information about the capacitor's voltage rating and the diode's reverse breakdown voltage. -IMPACT: Unable to fully assess the subcircuit's ability to withstand overvoltage transients without knowing the component ratings. -VERIFICATION: Obtain the capacitor's voltage rating and the diode's reverse breakdown voltage. -RECOMMENDATION: Provide the capacitor's voltage rating and the diode's reverse breakdown voltage to ensure the components can withstand expected overvoltage conditions. -STANDARDS: Relevant component-level standards for capacitors (e.g., IEC 60384-1) and diodes (e.g., IEC 60747-1). -TRADE-OFFS: N/A -PRIORITY: Medium - -SEVERITY: Low -CATEGORY: Design -SUBCIRCUIT: top.complete_circuit0.power_section0.input_protection0 -DESCRIPTION: Lack of information about the expected input current range. -IMPACT: Unable to fully assess the subcircuit's ability to handle high input currents without knowing the expected current range. -VERIFICATION: Obtain the expected input current range for the system. -RECOMMENDATION: Provide the expected input current range to ensure the subcircuit can handle the current levels without excessive voltage drop or heating. -STANDARDS: Relevant industry standards for input current handling (e.g., IEC 61000-4-11 for voltage dips and interruptions). -TRADE-OFFS: N/A -PRIORITY: Low +DESCRIPTION: Limited protection scheme with single diode +IMPACT: Potential insufficient protection against reverse polarity and transients +RECOMMENDATION: Add TVS diode and series resistance + +Detailed Analysis: + +1. Protection Circuit Analysis: +- C1 (100µF) provides input filtering and transient suppression +- D1 (1N4148W) characteristics: + * Forward voltage: ~0.7V @ 20mA + * Max reverse voltage: 75V + * Max forward current: 150mA + * Recovery time: 4ns +- Filtering effectiveness: τ = RC (requires source impedance information) +- Limited reverse polarity protection through single diode + +2. Power Distribution Concerns: +- Missing current limiting elements +- No defined voltage rating for capacitor +- Potential inrush current issues with large capacitance +- Calculated maximum surge current through diode: + dI/dt = dV/L (requires input voltage specifications) + +3. Signal Integrity: +- Capacitor ESR not specified (critical for filtering performance) +- Resonance potential with undefined source inductance +- Filter corner frequency: f = 1/(2π√LC) (requires complete circuit values) + +Recommendations: +1. Add series current-limiting resistor (10-100Ω suggested) +2. Include bidirectional TVS diode for transient protection +3. Specify capacitor voltage rating ≥2x maximum input voltage +4. Consider adding parallel bleeder resistor +5. Specify capacitor ESR requirements Additional Information Needed: -1. Input voltage range and expected transient conditions (overvoltage, reverse polarity, etc.) -2. System-level input protection requirements -3. Capacitor voltage rating -4. Diode reverse breakdown voltage -5. Expected input current range +1. Input voltage range specifications +2. Maximum current requirements +3. Expected transient conditions +4. Environmental requirements +5. Source impedance characteristics +6. Load characteristics +7. PCB layout constraints +8. Thermal requirements +9. EMC/EMI requirements +10. Safety standards compliance needs + +Manufacturing Considerations: +- C1 footprint (CP_Radial_D10.0mm_P5.00mm) requires adequate board space +- D1 SOD-123 package allows automated assembly +- Consider adding test points for voltage monitoring +- Thermal relief required for capacitor pads + +Standards Compliance Gaps: +- Unable to assess without complete system requirements +- Potential IEC 61000-4-2 ESD protection inadequacy +- Limited compliance with reverse polarity protection standards + +Without additional system specifications, this analysis represents a preliminary assessment based on component characteristics and typical protection circuit requirements. ==================== top.complete_circuit0.power_section0.rc_filter0 ==================== Executive Summary: +This appears to be an RC low-pass filter circuit consisting of a 10kΩ resistor and 0.1µF capacitor. The configuration suggests power supply filtering or signal conditioning applications. -The provided circuit is a simple RC low-pass filter, commonly used for power supply decoupling or noise filtering applications. The filter consists of a 10kΩ resistor (R1) and a 0.1μF capacitor (C4) connected in series. The cutoff frequency of this filter is approximately 159Hz, which makes it suitable for attenuating high-frequency noise on power supply lines or filtering unwanted high-frequency signals. - -Critical Findings Summary: - -1. SEVERITY: Medium, CATEGORY: Design, SUBCIRCUIT: RC Filter - DESCRIPTION: The filter cutoff frequency may be too low for some applications, allowing low-frequency noise or ripple to pass through. - IMPACT: Potential power supply noise or ripple in the passband, affecting sensitive circuits. - VERIFICATION: Analyze the noise spectrum and frequency requirements of the circuits being powered. - RECOMMENDATION: Adjust the RC values to achieve a higher cutoff frequency if needed, balancing attenuation and stability requirements. - STANDARDS: IPC-9592 (Design and Assembly Process Implementation for Passive Components) - TRADE-OFFS: Higher cutoff frequency may reduce attenuation at high frequencies, requiring additional filtering stages. - PRIORITY: Medium - -2. SEVERITY: Low, CATEGORY: Manufacturing, SUBCIRCUIT: RC Filter - DESCRIPTION: Component tolerances may affect the actual filter characteristics. - IMPACT: Deviation from the designed cutoff frequency and attenuation response. - VERIFICATION: Measure the actual component values and calculate the resulting filter response. - RECOMMENDATION: Specify tight tolerance components or consider adding an adjustment mechanism if precise filtering is required. - STANDARDS: IPC-7351B (Generic Requirements for Surface Mount Design and Land Pattern Standard) - TRADE-OFFS: Tighter tolerances may increase component cost. - PRIORITY: Low - -Detailed Subcircuit Analysis: - -RC Filter (top.complete_circuit0.power_section0.rc_filter0): -The RC filter subcircuit consists of a 10kΩ resistor (R1) and a 0.1μF capacitor (C4) connected in series. This configuration forms a first-order low-pass filter with a cutoff frequency (f_c) calculated as: - -f_c = 1 / (2π × R × C) - = 1 / (2π × 10kΩ × 0.1μF) - ≈ 159Hz - -The filter will attenuate signals above the cutoff frequency at a rate of -20dB per decade. The attenuation at a specific frequency (f) can be calculated as: - -Attenuation (dB) = -20 × log(f / f_c) - -For example, at 1kHz, the attenuation will be approximately -16dB, and at 10kHz, it will be -36dB. - -The filter's time constant (τ) is given by: - -τ = R × C - = 10kΩ × 0.1μF - = 1ms - -This time constant determines the settling time of the filter's transient response. - -System-Level Analysis: - -Based on the provided information, this is a standalone RC filter subcircuit, and no system-level analysis can be performed without additional context. However, some general considerations for system integration are: - -1. Interface Analysis: The filter's input and output connections should be properly matched to the source and load impedances to avoid reflections and signal integrity issues. - -2. Bypass Capacitors: Additional bypass capacitors may be required in parallel with the filter capacitor to provide high-frequency decoupling and improve stability. - -3. Noise Sources: The effectiveness of the filter depends on the frequency spectrum of the noise or ripple present on the input signal. Higher frequencies will be attenuated more effectively. - -4. Failure Modes: Open or short circuit failures of the filter components can cause complete loss of filtering or impedance mismatch, respectively. - -5. Performance Bottlenecks: For applications requiring significant noise attenuation across a wide frequency range, a single RC filter may not be sufficient, and additional filtering stages or alternative filter topologies may be necessary. - -Cross-Cutting Concerns: - -1. Component Aging: Capacitors and resistors can degrade over time, affecting the filter's performance. Periodic testing and component replacement may be required in critical applications. +Critical Findings: +1. Cutoff frequency: fc = 1/(2π×R×C) = 159.2 Hz +2. Time constant: τ = R×C = 1ms +3. Basic power supply ripple attenuation capability: -20dB/decade above cutoff -2. Temperature Effects: The temperature coefficients of the components can cause the filter characteristics to drift with temperature changes. +Detailed Circuit Analysis: -3. EMI/EMC: While the filter can attenuate conducted noise, it may not provide sufficient protection against radiated electromagnetic interference (EMI). Additional shielding or EMI countermeasures may be required. +1. Filter Characteristics: +- Configuration: First-order low-pass RC filter +- Cutoff frequency: 159.2 Hz +- Attenuation slope: -20 dB/decade +- Phase shift at cutoff: -45 degrees +- Input impedance at DC: 10kΩ +- Output impedance varies with frequency from near 0Ω (DC) to 10kΩ (high frequency) -Recommendations Summary: - -1. Analyze the noise spectrum and frequency requirements of the circuits being powered to determine if the filter's cutoff frequency is appropriate. - -2. Specify tight tolerance components or consider adding an adjustment mechanism if precise filtering is required. - -3. Implement additional bypass capacitors in parallel with the filter capacitor for improved high-frequency decoupling. - -4. Consider alternative filter topologies or cascaded filter stages if broader frequency attenuation is needed. - -5. Implement periodic testing and component replacement procedures for critical applications to mitigate aging effects. - -6. Consider temperature compensation or derating if the filter will operate in a wide temperature range. - -7. Evaluate the need for additional EMI/EMC countermeasures based on the system's electromagnetic environment. - -Required Action Items (Prioritized): - -1. Analyze the noise spectrum and frequency requirements of the circuits being powered to determine if the filter's cutoff frequency is appropriate. (High Priority) - -2. Consider adding additional bypass capacitors in parallel with the filter capacitor for improved high-frequency decoupling. (Medium Priority) +SEVERITY: Medium +CATEGORY: Performance +DESCRIPTION: Relatively high cutoff frequency may not provide adequate filtering for some power applications +RECOMMENDATION: Consider increasing C value if lower frequency filtering required -3. Evaluate the need for alternative filter topologies or cascaded filter stages if broader frequency attenuation is needed. (Medium Priority) +2. Component Selection: -4. Specify tight tolerance components or consider adding an adjustment mechanism if precise filtering is required. (Low Priority) +Capacitor C4: +- 0805 package suitable for automated assembly +- 0.1µF standard value allows good availability +- Voltage rating not specified (critical missing information) +- Temperature coefficient not specified -5. Implement periodic testing and component replacement procedures for critical applications to mitigate aging effects. (Low Priority) +Resistor R1: +- 0805 package compatible with standard assembly +- 10kΩ value provides reasonable compromise between filtering and voltage drop +- Power rating not specified (critical missing information) -6. Consider temperature compensation or derating if the filter will operate in a wide temperature range. (Low Priority) +3. Implementation Concerns: -7. Evaluate the need for additional EMI/EMC countermeasures based on the system's electromagnetic environment. (Low Priority) +Power Handling: +SEVERITY: High +CATEGORY: Safety +DESCRIPTION: Without voltage/power ratings, cannot verify safe operation +RECOMMENDATION: Specify maximum operating voltage and current + +Layout Considerations: +- Keep capacitor close to load +- Minimize trace inductance +- Consider guard rings for sensitive applications +- Ground plane connectivity critical + +Recommendations: +1. Specify maximum operating voltage +2. Add voltage rating for C4 +3. Consider X7R or better dielectric for stability +4. Add power rating for R1 +5. Consider adding second stage for better attenuation if needed + +Required Action Items: +1. Define maximum operating voltage and current +2. Specify capacitor voltage rating +3. Specify resistor power rating +4. Review PCB layout guidelines Additional Information Needed: - -To provide a more comprehensive analysis and detailed recommendations, the following additional information would be helpful: - -1. Details of the circuits or systems being powered by this filter, including their noise sensitivity and frequency requirements. - -2. Specifications or requirements for the level of noise attenuation or filtering needed. - -3. Expected environmental conditions (temperature range, humidity, vibration, etc.) for the system's operation. - -4. EMI/EMC requirements or standards that the system must comply with. - -5. Details of the power supply or source supplying the input to the filter, including its noise characteristics and ripple frequency. - -6. Any constraints or requirements related to cost, size, weight, or power consumption that may impact component selection or filter design. - -7. Information on the system's layout, grounding, and shielding strategies, which can affect noise coupling and filter performance. - -With this additional information, a more comprehensive analysis and tailored recommendations can be provided, considering the specific application requirements and system constraints. +1. Maximum operating voltage? +2. Expected ripple current? +3. Required attenuation at specific frequencies? +4. Operating temperature range? +5. Power supply characteristics? +6. Load characteristics? +7. EMI/EMC requirements? +8. Environmental conditions? +9. Expected lifetime requirements? +10. Cost constraints? + +Performance Limitations: +- Limited high-frequency attenuation +- Susceptible to component tolerance variations +- No protection against reverse voltage +- No overload protection +- Temperature dependency of component values + +This basic filter design is suitable for general power supply filtering but may need enhancement depending on specific requirements. Additional information would enable more detailed recommendations for optimization. ==================== top.complete_circuit0.power_section0.voltage_regulator0 ==================== -0. System-Level Analysis: - -While the provided information focuses on the voltage regulator subcircuit, there is insufficient data to conduct a comprehensive system-level analysis. However, based on the available details, a few observations can be made: - -- The voltage regulator subcircuit appears to be a linear regulator implementation using the LM7805 positive voltage regulator IC, intended to provide a regulated 5V output. -- The capacitors C2 and C3 (10uF each) are likely used for input and output filtering, respectively. -- Without information about the rest of the system, it is challenging to assess system-level timing, synchronization, resource allocation, failure modes, integration challenges, performance bottlenecks, or scalability. - -1. Comprehensive Design Architecture Review: - -The provided information is limited to a voltage regulator subcircuit, making it difficult to evaluate the overall design architecture. However, some observations can be made: - -- The hierarchical structure seems to follow a top-down approach, with the voltage regulator being a part of the power_section0 block, which is part of the complete_circuit0 block. -- The use of a linear regulator (LM7805) simplifies the design but may have lower efficiency compared to switching regulators, depending on the application's power requirements. -- Without additional context, it is challenging to assess modularity, reusability, interface protocols, control paths, design patterns, critical paths, feedback loops, clock domains, reset strategy, state machines, resource utilization, or design rule compliance. - -2. In-depth Power Distribution Analysis: - -- The voltage regulator subcircuit appears to be a linear regulator design, which will have inherent efficiency limitations due to the voltage drop across the regulator. -- Input and output capacitors (C2 and C3) are present for filtering, but their effectiveness will depend on the specific application's load transient requirements. -- Without information about the input voltage source, load requirements, and overall power distribution architecture, it is difficult to perform comprehensive power distribution analysis, such as voltage drop calculations, current distribution, power sequencing, brownout behavior, load transient response, power supply rejection ratio, efficiency optimization, or thermal implications. +Executive Summary: +This is a basic linear voltage regulator circuit using an LM7805 to provide a regulated 5V output with input/output capacitive filtering. The design represents a standard implementation but has several areas requiring attention. +Critical Findings Summary: +1. SEVERITY: High +CATEGORY: Design +DESCRIPTION: Input voltage range unspecified +IMPACT: Could lead to excessive power dissipation +RECOMMENDATION: Specify input voltage range and verify thermal management + +2. SEVERITY: Medium +CATEGORY: Performance +DESCRIPTION: Capacitor values may be insufficient for stability +IMPACT: Potential oscillation under certain load conditions +RECOMMENDATION: Increase output capacitor to 22µF minimum + +Detailed Analysis: + +Power Section Analysis: +- LM7805 provides regulated +5V ±4% output +- Maximum current capability: 1.5A +- Dropout voltage: 2V typical +- Power dissipation (worst case) = (Vin - 5V) × Iout +- Thermal resistance junction-to-ambient: 65°C/W (TO-220 without heatsink) + +Capacitor Configuration: +- Input capacitor (C2): 10µF +- Output capacitor (C3): 10µF +- ESR requirements not specified +- Minimum recommended output capacitance: 22µF per datasheet +- Current design may experience stability issues + +System Performance: +- Load regulation: 0.1% typical +- Line regulation: 0.02%/V typical +- Ripple rejection: 78dB typical at 120Hz +- Output noise: 40µV typical (10Hz to 100kHz) + +Critical Issues: + +1. Thermal Management: SEVERITY: High -CATEGORY: Performance, Design -SUBCIRCUIT: top.complete_circuit0.power_section0.voltage_regulator0 -DESCRIPTION: Linear regulator efficiency limitations and potential voltage drop issues. -IMPACT: Potential power losses, thermal dissipation, and voltage regulation issues under heavy load conditions. -VERIFICATION: Conduct load testing and measure efficiency, voltage drop, and thermal performance across the expected operating range. -RECOMMENDATION: Consider using a switching regulator topology if efficiency and heat dissipation are critical concerns. Alternatively, evaluate the feasibility of a low-dropout regulator (LDO) if the input-output voltage differential is small. -STANDARDS: Relevant power supply design standards and application-specific requirements. -TRADE-OFFS: Switching regulators may introduce additional complexity, noise, and cost, but can significantly improve efficiency. LDOs may offer better efficiency than linear regulators while maintaining simplicity, but have limited input-output voltage differential. -PRIORITY: High, depending on power requirements and efficiency constraints. - -3. Detailed Signal Integrity Analysis: - -Without information about the specific signals, interconnects, and layout, it is challenging to perform a comprehensive signal integrity analysis. However, some general observations can be made: - -- The voltage regulator subcircuit itself does not involve high-speed signals, reducing the likelihood of significant signal integrity issues within this block. -- However, the regulated output voltage (N$7) may be routed to various loads, and signal integrity could be affected by factors such as trace lengths, impedance discontinuities, and crosstalk. -- If the regulator output is used for analog or mixed-signal applications, power supply noise and ripple may need to be considered and filtered appropriately. - -4. Thermal Performance Analysis: - -- Linear regulators, like the LM7805, dissipate power in the form of heat due to the voltage drop across the regulator. -- The thermal performance will depend on factors such as the input-output voltage differential, load current, package type (TO-220 in this case), and the presence of a heatsink or other cooling mechanisms. -- Without information about the expected load conditions, input voltage, and thermal environment, it is difficult to quantify the temperature rise or assess the need for additional cooling measures. +- Maximum junction temperature: 125°C +- Thermal protection activates at 165°C +- Heatsink requirements cannot be calculated without input voltage specification +2. Stability Concerns: SEVERITY: Medium -CATEGORY: Thermal -SUBCIRCUIT: top.complete_circuit0.power_section0.voltage_regulator0 -DESCRIPTION: Potential thermal issues due to power dissipation in the linear regulator. -IMPACT: Excessive temperature rise can lead to reduced reliability, performance degradation, or device failure. -VERIFICATION: Conduct thermal testing under various load conditions and measure the temperature rise of the regulator package. -RECOMMENDATION: Evaluate the need for a heatsink or other cooling mechanisms based on the expected power dissipation and thermal environment. Consider derating the regulator's maximum output current if necessary. -STANDARDS: Manufacturer's recommended operating conditions, thermal design guidelines, and application-specific thermal requirements. -TRADE-OFFS: Adding a heatsink or active cooling may increase cost and complexity but will improve thermal performance and reliability. -PRIORITY: Medium to high, depending on the expected power dissipation and thermal constraints. - -5. Comprehensive Noise Analysis: - -- Linear regulators generally have good power supply rejection ratio (PSRR) and low output noise compared to switching regulators. -- The input and output capacitors (C2 and C3) help filter high-frequency noise on the input and output, respectively. -- However, without information about the specific noise sources, coupling paths, and sensitive circuits, it is difficult to perform a comprehensive noise analysis. -- If the regulated output is used for analog or mixed-signal applications, additional filtering or decoupling may be required to meet noise and ripple requirements. - -6. Testing and Verification Strategy: - -- Functional testing: Verify the voltage regulator's output voltage and regulation accuracy under various load conditions. -- Performance testing: Measure efficiency, load regulation, line regulation, and transient response. -- Environmental testing: Evaluate the regulator's performance across the expected temperature range and other environmental conditions. -- Reliability testing: Conduct accelerated life testing or stress testing to assess long-term reliability. -- EMC/EMI testing: Evaluate the regulator's electromagnetic compatibility and emissions, if applicable. -- Production testing: Develop a strategy for testing and calibrating the regulator during manufacturing. -- Diagnostic capabilities: Implement test points or diagnostic interfaces for troubleshooting and field maintenance. +- Current output capacitance (10µF) below recommended minimum +- ESR specifications critical for stability +- Phase margin potentially compromised + +Required Action Items: +1. Specify input voltage range +2. Increase C3 to minimum 22µF +3. Add heatsink calculations based on input voltage +4. Specify ESR requirements for capacitors +5. Add reverse voltage protection +6. Consider adding input/output protection Additional Information Needed: - -To provide more comprehensive analysis and actionable insights, the following additional information would be beneficial: - -1. System-level requirements and specifications, including power budget, load profiles, and operating conditions. -2. Schematic and layout details of the complete system, including power distribution architecture and interconnects. -3. Expected input voltage range and characteristics (e.g., ripple, transients). -4. Thermal environment and cooling solutions (if any) for the overall system. -5. Noise sources, sensitive circuits, and noise mitigation requirements. -6. Regulatory compliance and industry standards applicable to the product. -7. Manufacturing and testing strategies, quality control measures, and cost constraints. -8. Maintenance and field service requirements for the product. - -With this additional information, a more thorough analysis can be conducted, addressing system-level concerns, cross-cutting issues, and providing more specific and actionable recommendations tailored to the product's requirements and constraints. +1. Input voltage range specification +2. Maximum load current requirement +3. Operating temperature range +4. PCB thermal characteristics +5. Load transient requirements +6. EMI/EMC requirements +7. Expected ambient temperature +8. Required MTBF +9. Environmental conditions +10. Cost constraints + +Recommendations: +1. Add reverse polarity protection diode +2. Include input transient protection +3. Add output short circuit protection +4. Increase output capacitance to 22µF minimum +5. Consider adding status LED +6. Add test points for voltage measurements +7. Consider switching regulator alternative for better efficiency + +The current design represents a functional but basic implementation that requires additional specification and protection features for robust operation in a practical application. From 9826bda619fd199f788fc5a2ba1736b52b9dfaa3 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 11:25:12 -0800 Subject: [PATCH 15/73] update max tokens, delete unused files --- circuit_description.txt | 286 ------------- circuit_llm_analysis.txt | 0 skidl_llm_test.py | 3 +- src/skidl/circuit_analyzer.py | 2 +- subcircuits_analysis.txt | 737 ++++++++++++++++------------------ 5 files changed, 345 insertions(+), 683 deletions(-) delete mode 100644 circuit_description.txt delete mode 100644 circuit_llm_analysis.txt diff --git a/circuit_description.txt b/circuit_description.txt deleted file mode 100644 index d6432472a..000000000 --- a/circuit_description.txt +++ /dev/null @@ -1,286 +0,0 @@ -Circuit Description: -======================================== -Circuit Name: -Top Level Hierarchy: top - -Hierarchy Details: ----------------------------------------- -Hierarchy Level: top.complete_circuit0.double_divider0.output_termination0 -Parts: - Part: R6 - Name: R - Value: 100K - Footprint: Resistor_SMD.pretty:R_0805_2012Metric - Pins: - 1/~: N$4 - 2/~: N$5 - Nets: - Net: N$4 - Local Connections: - R6.1/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.voltage_divider1: - R4.2/~ - R5.1/~ - Net: N$5 - Local Connections: - R6.2/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.voltage_divider0: - R3.2/~ - top.complete_circuit0.double_divider0.voltage_divider1: - R5.2/~ - top.complete_circuit0.power_section0.input_protection0: - C1.2/~ - top.complete_circuit0.power_section0.rc_filter0: - C4.2/~ - top.complete_circuit0.power_section0.voltage_regulator0: - C2.2/~ - C3.2/~ - U1.2/GND -_____________________________________________________ -Hierarchy Level: top.complete_circuit0.double_divider0.voltage_divider0 -Parts: - Part: R2 - Name: R - Value: 1K - Footprint: Resistor_SMD.pretty:R_0805_2012Metric - Pins: - 1/~: N$3 - 2/~: N$10 - Part: R3 - Name: R - Value: 500 - Footprint: Resistor_SMD.pretty:R_0805_2012Metric - Pins: - 1/~: N$10 - 2/~: N$5 - Nets: - Net: N$3 - Local Connections: - R2.1/~ - Connected to Other Hierarchies: - top.complete_circuit0.power_section0.rc_filter0: - C4.1/~ - R1.2/~ - Net: N$5 - Local Connections: - R3.2/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.output_termination0: - R6.2/~ - top.complete_circuit0.double_divider0.voltage_divider1: - R5.2/~ - top.complete_circuit0.power_section0.input_protection0: - C1.2/~ - top.complete_circuit0.power_section0.rc_filter0: - C4.2/~ - top.complete_circuit0.power_section0.voltage_regulator0: - C2.2/~ - C3.2/~ - U1.2/GND - Net: N$9 - Local Connections: - R2.2/~ - R3.1/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.voltage_divider1: - R4.1/~ -_____________________________________________________ -Hierarchy Level: top.complete_circuit0.double_divider0.voltage_divider1 -Parts: - Part: R4 - Name: R - Value: 1K - Footprint: Resistor_SMD.pretty:R_0805_2012Metric - Pins: - 1/~: N$9 - 2/~: N$11 - Part: R5 - Name: R - Value: 500 - Footprint: Resistor_SMD.pretty:R_0805_2012Metric - Pins: - 1/~: N$11 - 2/~: N$5 - Nets: - Net: N$4 - Local Connections: - R4.2/~ - R5.1/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.output_termination0: - R6.1/~ - Net: N$5 - Local Connections: - R5.2/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.output_termination0: - R6.2/~ - top.complete_circuit0.double_divider0.voltage_divider0: - R3.2/~ - top.complete_circuit0.power_section0.input_protection0: - C1.2/~ - top.complete_circuit0.power_section0.rc_filter0: - C4.2/~ - top.complete_circuit0.power_section0.voltage_regulator0: - C2.2/~ - C3.2/~ - U1.2/GND - Net: N$9 - Local Connections: - R4.1/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.voltage_divider0: - R2.2/~ - R3.1/~ -_____________________________________________________ -Hierarchy Level: top.complete_circuit0.power_section0.input_protection0 -Parts: - Part: C1 - Name: C - Value: 100uF - Footprint: Capacitor_THT:CP_Radial_D10.0mm_P5.00mm - Pins: - 1/~: N$1 - 2/~: N$5 - Part: D1 - Name: D - Value: 1N4148W - Footprint: Diode_SMD:D_SOD-123 - Pins: - 1/K: N$6 - 2/A: N$1 - Nets: - Net: N$1 - Local Connections: - C1.1/~ - D1.2/A - Net: N$5 - Local Connections: - C1.2/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.output_termination0: - R6.2/~ - top.complete_circuit0.double_divider0.voltage_divider0: - R3.2/~ - top.complete_circuit0.double_divider0.voltage_divider1: - R5.2/~ - top.complete_circuit0.power_section0.rc_filter0: - C4.2/~ - top.complete_circuit0.power_section0.voltage_regulator0: - C2.2/~ - C3.2/~ - U1.2/GND - Net: N$6 - Local Connections: - D1.1/K - Connected to Other Hierarchies: - top.complete_circuit0.power_section0.voltage_regulator0: - C2.1/~ - U1.1/VI -_____________________________________________________ -Hierarchy Level: top.complete_circuit0.power_section0.rc_filter0 -Parts: - Part: C4 - Name: C - Value: 0.1uF - Footprint: Capacitor_SMD:C_0805_2012Metric - Pins: - 1/~: N$8 - 2/~: N$5 - Part: R1 - Name: R - Value: 10K - Footprint: Resistor_SMD.pretty:R_0805_2012Metric - Pins: - 1/~: N$2 - 2/~: N$8 - Nets: - Net: N$2 - Local Connections: - R1.1/~ - Connected to Other Hierarchies: - top.complete_circuit0.power_section0.voltage_regulator0: - C3.1/~ - U1.3/VO - Net: N$3 - Local Connections: - C4.1/~ - R1.2/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.voltage_divider0: - R2.1/~ - Net: N$5 - Local Connections: - C4.2/~ - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.output_termination0: - R6.2/~ - top.complete_circuit0.double_divider0.voltage_divider0: - R3.2/~ - top.complete_circuit0.double_divider0.voltage_divider1: - R5.2/~ - top.complete_circuit0.power_section0.input_protection0: - C1.2/~ - top.complete_circuit0.power_section0.voltage_regulator0: - C2.2/~ - C3.2/~ - U1.2/GND -_____________________________________________________ -Hierarchy Level: top.complete_circuit0.power_section0.voltage_regulator0 -Parts: - Part: C2 - Name: C - Value: 10uF - Footprint: Capacitor_SMD:C_0805_2012Metric - Pins: - 1/~: N$6 - 2/~: N$5 - Part: C3 - Name: C - Value: 10uF - Footprint: Capacitor_SMD:C_0805_2012Metric - Pins: - 1/~: N$7 - 2/~: N$5 - Part: U1 - Name: LM7805_TO220 - Value: LM7805_TO220 - Footprint: Package_TO_SOT_THT:TO-220-3_Vertical - Pins: - 1/VI: N$6 - 2/GND: N$5 - 3/VO: N$7 - Nets: - Net: N$2 - Local Connections: - C3.1/~ - U1.3/VO - Connected to Other Hierarchies: - top.complete_circuit0.power_section0.rc_filter0: - R1.1/~ - Net: N$5 - Local Connections: - C2.2/~ - C3.2/~ - U1.2/GND - Connected to Other Hierarchies: - top.complete_circuit0.double_divider0.output_termination0: - R6.2/~ - top.complete_circuit0.double_divider0.voltage_divider0: - R3.2/~ - top.complete_circuit0.double_divider0.voltage_divider1: - R5.2/~ - top.complete_circuit0.power_section0.input_protection0: - C1.2/~ - top.complete_circuit0.power_section0.rc_filter0: - C4.2/~ - Net: N$6 - Local Connections: - C2.1/~ - U1.1/VI - Connected to Other Hierarchies: - top.complete_circuit0.power_section0.input_protection0: - D1.1/K -=============== END CIRCUIT =============== \ No newline at end of file diff --git a/circuit_llm_analysis.txt b/circuit_llm_analysis.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/skidl_llm_test.py b/skidl_llm_test.py index 712c33f57..ab1ed30bf 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -102,7 +102,8 @@ def complete_circuit(): print(f"\nSubcircuit: {hier}") if analysis["success"]: print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - print(f"Tokens used: {analysis['prompt_tokens'] + analysis['response_tokens']}") + tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) + print(f"Tokens used: {tokens}") else: print(f"Analysis failed: {analysis['error']}") diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index d18f7a2d9..fb12c760b 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -105,7 +105,7 @@ def _process_openrouter(self, "model": self.model, "messages": messages, "temperature": 0.7, - "max_tokens": 4096, + "max_tokens": 8000, "headers": { # OpenRouter-specific headers in the request body "HTTP-Referer": "https://github.com/devbisme/skidl", "X-Title": "SKiDL Circuit Analyzer" diff --git a/subcircuits_analysis.txt b/subcircuits_analysis.txt index 8e5bd1921..bf1898622 100644 --- a/subcircuits_analysis.txt +++ b/subcircuits_analysis.txt @@ -2,420 +2,367 @@ ==================== top.complete_circuit0.double_divider0.output_termination0 ==================== -Executive Summary: -This appears to be an output termination subcircuit utilizing a single 100kΩ resistor (R6) in a double divider configuration. The limited information suggests this is likely a voltage divider or signal termination network. +EXECUTIVE SUMMARY: +Limited circuit information prevents comprehensive analysis. The provided description focuses on a single resistor (R6) in a termination subcircuit, requiring additional context for meaningful evaluation. -Critical Findings Summary: -SEVERITY: Medium -CATEGORY: Design -DESCRIPTION: Single resistor termination may be insufficient for proper impedance matching -IMPACT: Potential signal reflection and integrity issues -RECOMMENDATION: Consider matched termination network based on transmission line impedance - -Detailed Subcircuit Analysis: - -1. Component Analysis: -- R6 (100kΩ) is significantly higher than typical termination values (50Ω-120Ω) -- SMD 0805 package suggests modern surface mount design -- Power rating for 0805: typically 0.125W-0.25W at 70°C - -2. Signal Integrity Concerns: -- High impedance termination may lead to reflections -- Calculated maximum frequency for reliable operation (assuming 10pF parasitic): - f = 1/(2π * 100kΩ * 10pF) ≈ 159kHz -- Signal rise time limitations: tr = 2.2 * R * C ≈ 2.2μs - -3. Power Dissipation: -- Maximum power in R6 (at 5V): - P = V²/R = 25V/100kΩ = 0.25mW -- Well within 0805 package ratings - -System-Level Implications: -1. Bandwidth limitations due to high impedance -2. Potential noise susceptibility -3. Limited drive capability -4. High input impedance may provide good isolation - -Recommendations: -1. Review intended signal characteristics and adjust termination accordingly -2. Consider parallel termination if dealing with transmission lines -3. Evaluate need for EMI/RFI protection -4. Consider adding bypass capacitor for high-frequency noise rejection - -Required Action Items: -1. Determine characteristic impedance of connected transmission line -2. Verify maximum operating frequency requirements -3. Assess noise immunity requirements -4. Review EMC compliance needs - -Additional Information Needed: -1. Signal type and characteristics (analog/digital, frequency, amplitude) -2. Source impedance specifications -3. Transmission line characteristics -4. EMC/EMI requirements -5. Operating environment conditions -6. Complete circuit context and interfacing signals -7. Power supply specifications -8. Maximum allowable signal distortion -9. Required bandwidth -10. System timing constraints -11. Environmental requirements -12. Overall system architecture -13. Connected subcircuits -14. PCB layout constraints -15. Reliability requirements - -Without additional context, this analysis is necessarily limited. The high impedance value suggests this may be part of a larger biasing or filtering network rather than a traditional transmission line termination. +CRITICAL FINDINGS SUMMARY: +- Insufficient data for complete system analysis +- Single component (R6) provides minimal insight +- Termination network details are incomplete -==================== top.complete_circuit0.double_divider0.voltage_divider0 ==================== -Executive Summary: -This circuit appears to be a basic voltage divider configuration utilizing two resistors (R2: 1kΩ, R3: 500Ω) in series. The voltage division ratio is 1:1.5, providing 33.33% of input voltage at the output node N$10. +COMPONENT ANALYSIS: R6 +- Resistance Value: 100 kΩ +- Footprint: 0805 SMD +- Connectivity: Connected between nodes N$4 and N$5 -Critical Findings: +SIGNAL TERMINATION PRELIMINARY ASSESSMENT: SEVERITY: Medium -CATEGORY: Design -DESCRIPTION: Power dissipation and thermal management may be concerns depending on input voltage -IMPACT: Potential reliability issues if input voltage exceeds power rating of resistors +CATEGORY: Signal Integrity +DESCRIPTION: Potential output termination resistance network +POTENTIAL FUNCTIONS: +1. Impedance matching +2. Signal reflection reduction +3. Noise suppression + +RECOMMENDATIONS: +1. Obtain complete circuit schematic +2. Verify termination network topology +3. Confirm signal characteristics and frequency range +4. Validate impedance matching requirements + +ADDITIONAL INFORMATION NEEDED: +- Full circuit schematic +- Signal frequency and type +- Transmission line characteristics +- Adjacent circuit component details +- Operating voltage range +- Specific application context + +Without additional context, a comprehensive analysis is not possible. The single resistor suggests an output termination network, but precise characterization requires more information about the signal path, frequency, and system requirements. -Detailed Analysis: +==================== top.complete_circuit0.double_divider0.voltage_divider0 ==================== +EXECUTIVE SUMMARY: +The provided circuit represents a voltage divider subcircuit with two resistive elements (R2 and R3) configured in series. Preliminary analysis reveals a basic voltage scaling network with potential signal conditioning or reference voltage generation functionality. + +CRITICAL FINDINGS SUMMARY: +1. Voltage Divider Configuration Detected +2. Resistor Values: R2 = 1kΩ, R3 = 500Ω +3. Potential Signal Conditioning Application + +DETAILED SUBCIRCUIT ANALYSIS: + +Voltage Divider Characteristics: +- Total Resistance: 1.5kΩ +- Voltage Division Ratio Calculation: + Vout = Vin * (R3 / (R2 + R3)) + Vout = Vin * (500 / (1000 + 500)) + Vout = Vin * (500 / 1500) + Vout = Vin * 0.333 (33.3% voltage scaling) + +SIGNAL INTEGRITY ANALYSIS: +- Resistance Tolerance Impact: Assuming ±5% resistor tolerance +- Potential Voltage Variation: ±1.65% of output voltage +- Node Connections: N$3, N$10, N$5 identified + +POWER DISTRIBUTION ANALYSIS: +- Power Dissipation Calculation: + P = (Vin)² / R_total + Assumes maximum operating voltage and resistance + +RECOMMENDATIONS: +1. Verify precise voltage scaling requirements +2. Confirm resistor tolerance meets design specifications +3. Validate thermal performance under expected load conditions +4. Implement precision resistors if high accuracy is critical + +ADDITIONAL INFORMATION NEEDED: +1. Input voltage range +2. Target application context +3. Required output voltage precision +4. Operating temperature range +5. Expected current load -1. Voltage Division: -- Output voltage ratio: Vout/Vin = R3/(R2+R3) = 500/(1000+500) = 0.3333 -- Current through divider: I = Vin/(R2+R3) = Vin/1500Ω +SEVERITY MATRIX: +- Design Complexity: Low +- Performance Risk: Medium +- Implementation Difficulty: Low -2. Power Considerations: -- Power dissipation R2: P_R2 = I²×R2 = (Vin/1500)²×1000 -- Power dissipation R3: P_R3 = I²×R3 = (Vin/1500)²×500 -- For 0805 resistors, typical max power rating is 125mW +Detailed technical recommendations and comprehensive system-level analysis require additional contextual information about the broader circuit design and specific application requirements. -3. Implementation Concerns: -- SMD 0805 footprint selection is appropriate for most applications -- Voltage coefficient of resistance (VCR) effects minimal at typical voltages -- Temperature coefficient consideration needed for precision applications +==================== top.complete_circuit0.double_divider0.voltage_divider1 ==================== +EXECUTIVE SUMMARY +The analyzed circuit segment represents a voltage divider subcircuit with two resistive elements (R4 and R5) configured in series, intended for voltage scaling or signal conditioning purposes. -Recommendations: -1. Add input voltage specification to validate component ratings -2. Consider adding bypass capacitor for AC stability -3. Evaluate need for tighter tolerance resistors depending on application +CRITICAL FINDINGS SUMMARY: +1. Voltage Divider Configuration Detected +2. Potential Signal Conditioning Subcircuit +3. Requires Additional Context for Complete Analysis -Required Action Items: -1. Specify maximum input voltage -2. Define required output voltage accuracy -3. Consider adding temperature derating if operating in elevated temperatures +DETAILED SUBCIRCUIT ANALYSIS: -Additional Information Needed: -1. Input voltage range -2. Required output voltage accuracy -3. Operating temperature range -4. Load impedance requirements -5. AC performance requirements if applicable -6. Environmental conditions -7. Expected service life -8. Required reliability level +Voltage Divider Characteristics: +- R4: 1kΩ resistor +- R5: 500Ω resistor +- Connection Nodes: N$9 → N$11 → N$5 -This appears to be a subcircuit of a larger system (double_divider0), suggesting cascaded voltage division. Further analysis of the complete system would require additional circuit details. +Voltage Division Calculation: +Vout = Vin * (R5 / (R4 + R5)) +Voltage Scaling Factor: Approximately 0.333 (33.3%) -==================== top.complete_circuit0.double_divider0.voltage_divider1 ==================== -Executive Summary: -This is a basic voltage divider circuit consisting of two resistors (R4: 1kΩ, R5: 500Ω) in series, forming a 2:1 voltage division ratio. - -Critical Findings Summary: -1. SEVERITY: Medium -CATEGORY: Design -DESCRIPTION: No input protection or filtering components present -RECOMMENDATION: Add input protection and bypass capacitors - -2. SEVERITY: Low -CATEGORY: Performance -DESCRIPTION: Potential loading effects on output -RECOMMENDATION: Consider buffer amplifier for isolation - -Detailed Circuit Analysis: - -1. Voltage Division: -- Voltage division ratio = R5/(R4+R5) = 500/(1000+500) = 0.333 -- Output voltage = Vin × 0.333 -- Total current path resistance = 1.5kΩ - -2. Power Analysis: -- Maximum power dissipation (assuming 5V input): - * R4: P = V²/R = (3.33V)²/1000Ω = 11.1mW - * R5: P = (1.67V)²/500Ω = 5.57mW -- Well within typical 0805 package rating (125mW) - -3. Loading Effects: -- Output impedance = R4||R5 = (1000×500)/(1000+500) = 333.33Ω -- May require buffering depending on load requirements - -System-Level Concerns: -1. No input protection -2. No filtering for noise rejection -3. High output impedance may affect accuracy under load -4. Temperature coefficient matching between resistors not specified - -Recommendations: -1. Add input protection components -2. Consider adding bypass capacitors -3. Add buffer amplifier if driving variable loads -4. Use precision resistors with matched temperature coefficients -5. Consider 0.1% tolerance resistors for better accuracy - -Required Action Items: -1. Specify input voltage range -2. Define load requirements -3. Add protection circuitry -4. Consider adding buffer stage - -Additional Information Needed: -1. Input voltage range and specifications -2. Load impedance requirements -3. Accuracy requirements -4. Operating temperature range -5. Environmental conditions -6. EMI/EMC requirements -7. Power supply specifications -8. System grounding scheme -9. PCB layout constraints -10. Cost constraints - -Manufacturing Considerations: -- Standard 0805 footprint is manufacturing-friendly -- Consider using E96 series resistor values for better availability -- Recommend automated assembly compatible components -- Consider adding test points for production testing +SIGNAL CHARACTERISTICS: +- Low Impedance Output Path +- Simple Linear Voltage Reduction +- Potential Signal Conditioning Application -==================== top.complete_circuit0.power_section0.input_protection0 ==================== -Executive Summary: -This appears to be an input protection circuit consisting of a 100µF capacitor (C1) and 1N4148W diode (D1) in a power section subcircuit. The configuration suggests basic overvoltage protection and input filtering functionality. - -Critical Findings Summary: -SEVERITY: High -CATEGORY: Design -DESCRIPTION: Limited protection scheme with single diode -IMPACT: Potential insufficient protection against reverse polarity and transients -RECOMMENDATION: Add TVS diode and series resistance - -Detailed Analysis: - -1. Protection Circuit Analysis: -- C1 (100µF) provides input filtering and transient suppression -- D1 (1N4148W) characteristics: - * Forward voltage: ~0.7V @ 20mA - * Max reverse voltage: 75V - * Max forward current: 150mA - * Recovery time: 4ns -- Filtering effectiveness: τ = RC (requires source impedance information) -- Limited reverse polarity protection through single diode - -2. Power Distribution Concerns: -- Missing current limiting elements -- No defined voltage rating for capacitor -- Potential inrush current issues with large capacitance -- Calculated maximum surge current through diode: - dI/dt = dV/L (requires input voltage specifications) - -3. Signal Integrity: -- Capacitor ESR not specified (critical for filtering performance) -- Resonance potential with undefined source inductance -- Filter corner frequency: f = 1/(2π√LC) (requires complete circuit values) - -Recommendations: -1. Add series current-limiting resistor (10-100Ω suggested) -2. Include bidirectional TVS diode for transient protection -3. Specify capacitor voltage rating ≥2x maximum input voltage -4. Consider adding parallel bleeder resistor -5. Specify capacitor ESR requirements - -Additional Information Needed: -1. Input voltage range specifications -2. Maximum current requirements -3. Expected transient conditions -4. Environmental requirements -5. Source impedance characteristics -6. Load characteristics -7. PCB layout constraints -8. Thermal requirements -9. EMC/EMI requirements -10. Safety standards compliance needs - -Manufacturing Considerations: -- C1 footprint (CP_Radial_D10.0mm_P5.00mm) requires adequate board space -- D1 SOD-123 package allows automated assembly -- Consider adding test points for voltage monitoring -- Thermal relief required for capacitor pads - -Standards Compliance Gaps: -- Unable to assess without complete system requirements -- Potential IEC 61000-4-2 ESD protection inadequacy -- Limited compliance with reverse polarity protection standards - -Without additional system specifications, this analysis represents a preliminary assessment based on component characteristics and typical protection circuit requirements. +RECOMMENDATIONS: +1. Verify intended voltage scaling requirements +2. Confirm noise immunity and signal integrity +3. Validate thermal performance under expected load conditions -==================== top.complete_circuit0.power_section0.rc_filter0 ==================== -Executive Summary: -This appears to be an RC low-pass filter circuit consisting of a 10kΩ resistor and 0.1µF capacitor. The configuration suggests power supply filtering or signal conditioning applications. +CRITICAL MISSING INFORMATION: +1. Input voltage range +2. Maximum current requirements +3. Operating temperature specifications +4. Specific circuit application context +5. Full circuit schematic +6. Performance tolerance expectations -Critical Findings: -1. Cutoff frequency: fc = 1/(2π×R×C) = 159.2 Hz -2. Time constant: τ = R×C = 1ms -3. Basic power supply ripple attenuation capability: -20dB/decade above cutoff +ADDITIONAL INFORMATION NEEDED: +- Complete circuit context +- Specific design requirements +- Input signal characteristics +- Expected operating environment -Detailed Circuit Analysis: +Severity: Low-Medium +Confidence Level: Partial (Limited Context Available) -1. Filter Characteristics: -- Configuration: First-order low-pass RC filter -- Cutoff frequency: 159.2 Hz -- Attenuation slope: -20 dB/decade -- Phase shift at cutoff: -45 degrees -- Input impedance at DC: 10kΩ -- Output impedance varies with frequency from near 0Ω (DC) to 10kΩ (high frequency) +Detailed analysis requires supplementary circuit documentation for comprehensive evaluation. +==================== top.complete_circuit0.power_section0.input_protection0 ==================== +EXECUTIVE SUMMARY: +The analyzed circuit represents a basic input protection stage with minimal components, focusing on power supply conditioning and protection. + +CRITICAL FINDINGS SUMMARY: +- Simple input protection topology +- Potential voltage clamping and filtering capabilities +- Limited protection against transient events + +DETAILED SUBCIRCUIT ANALYSIS: + +Input Protection Subcircuit Components: +1. Capacitor C1 (100uF) + - Electrolytic capacitor + - Radial through-hole mounting + - Potential bulk filtering/decoupling function + - Large capacitance suggests power supply smoothing + +2. Diode D1 (1N4148W) + - Small signal switching diode + - Surface mount SOD-123 package + - Likely used for reverse polarity protection + - Low forward voltage drop (typical 0.7V) + +SYSTEM-LEVEL ANALYSIS: +- Basic input stage protection mechanism +- Rudimentary voltage regulation approach +- Limited transient suppression capabilities + +POWER DISTRIBUTION ANALYSIS: +- Diode introduces ~0.7V voltage drop +- Capacitor provides minimal energy storage +- No explicit voltage regulation + +SIGNAL INTEGRITY CONSIDERATIONS: +- Basic protection against reverse polarity +- Minimal noise suppression +- No explicit EMI/ESD protection + +RECOMMENDATIONS: + +1. Enhanced Protection Strategy SEVERITY: Medium -CATEGORY: Performance -DESCRIPTION: Relatively high cutoff frequency may not provide adequate filtering for some power applications -RECOMMENDATION: Consider increasing C value if lower frequency filtering required - -2. Component Selection: - -Capacitor C4: -- 0805 package suitable for automated assembly -- 0.1µF standard value allows good availability -- Voltage rating not specified (critical missing information) -- Temperature coefficient not specified - -Resistor R1: -- 0805 package compatible with standard assembly -- 10kΩ value provides reasonable compromise between filtering and voltage drop -- Power rating not specified (critical missing information) - -3. Implementation Concerns: - -Power Handling: -SEVERITY: High -CATEGORY: Safety -DESCRIPTION: Without voltage/power ratings, cannot verify safe operation -RECOMMENDATION: Specify maximum operating voltage and current - -Layout Considerations: -- Keep capacitor close to load -- Minimize trace inductance -- Consider guard rings for sensitive applications -- Ground plane connectivity critical - -Recommendations: -1. Specify maximum operating voltage -2. Add voltage rating for C4 -3. Consider X7R or better dielectric for stability -4. Add power rating for R1 -5. Consider adding second stage for better attenuation if needed - -Required Action Items: -1. Define maximum operating voltage and current -2. Specify capacitor voltage rating -3. Specify resistor power rating -4. Review PCB layout guidelines - -Additional Information Needed: -1. Maximum operating voltage? -2. Expected ripple current? -3. Required attenuation at specific frequencies? -4. Operating temperature range? -5. Power supply characteristics? -6. Load characteristics? -7. EMI/EMC requirements? -8. Environmental conditions? -9. Expected lifetime requirements? -10. Cost constraints? - -Performance Limitations: -- Limited high-frequency attenuation -- Susceptible to component tolerance variations -- No protection against reverse voltage -- No overload protection -- Temperature dependency of component values - -This basic filter design is suitable for general power supply filtering but may need enhancement depending on specific requirements. Additional information would enable more detailed recommendations for optimization. +RECOMMENDATION: +- Add TVS diode for improved transient protection +- Implement additional RC filtering +- Consider adding voltage regulator + +2. Component Selection +SEVERITY: Low +RECOMMENDATION: +- Replace 1N4148W with more robust protection diode +- Consider Schottky diode for lower voltage drop +- Evaluate capacitor's temperature and lifetime characteristics + +3. Design Robustness +SEVERITY: Medium +RECOMMENDATION: +- Add overcurrent protection +- Implement bidirectional protection +- Consider adding voltage clamp + +ADDITIONAL INFORMATION NEEDED: +1. Voltage input range +2. Maximum expected current +3. Operating environment details +4. Specific application context +5. Power supply characteristics + +PRIORITY ACTION ITEMS: +1. Add transient voltage suppressor +2. Implement bidirectional protection +3. Enhance filtering capabilities +4. Verify component ratings +5. Conduct comprehensive thermal analysis + +The current design represents a minimal input protection approach with significant opportunities for improvement in robustness, reliability, and performance. + +==================== top.complete_circuit0.power_section0.rc_filter0 ==================== +EXECUTIVE SUMMARY: +The provided circuit represents a basic RC filter subcircuit, characterized by minimal components and a simple topology. Initial analysis reveals a low-complexity power conditioning/filtering stage with potential signal conditioning applications. + +CRITICAL FINDINGS SUMMARY: +1. Simple RC Low-Pass Filter Configuration +2. Limited Filtering Capabilities +3. Potential Signal Conditioning Use Case + +DETAILED SUBCIRCUIT ANALYSIS: + +RC Filter Subcircuit Analysis: +------------------------------ +Components: +- Capacitor C4: 0.1µF +- Resistor R1: 10kΩ + +Filter Characteristics Calculation: +- Cutoff Frequency (fc) = 1 / (2π * R * C) +- fc = 1 / (2π * 10,000 * 0.1µF) +- fc ≈ 159.2 kHz + +Key Observations: +SEVERITY: Low +CATEGORY: Signal Conditioning +SUBCIRCUIT: RC Low-Pass Filter +DESCRIPTION: Basic first-order RC filter with moderate high-frequency attenuation +IMPACT: Provides gentle signal conditioning/noise reduction +VERIFICATION: Frequency response measurement +RECOMMENDATION: Consider application-specific requirements + +Performance Parameters: +- Attenuation Rate: -20 dB/decade +- Filter Order: First-Order +- Passband: DC to ~159 kHz +- Rolloff: Gradual + +SYSTEM-LEVEL ANALYSIS: + +Potential Applications: +1. Power supply noise reduction +2. Analog signal conditioning +3. Basic filtering for sensor interfaces +4. EMI/RFI mitigation + +MANUFACTURING CONSIDERATIONS: +- SMD 0805 footprint supports automated assembly +- Standard component values +- Low complexity reduces manufacturing variability + +RECOMMENDATIONS: +1. Verify specific system noise reduction requirements +2. Validate filter performance against application needs +3. Consider higher-order filtering if sharper rolloff required +4. Evaluate component tolerance impacts + +ADDITIONAL INFORMATION NEEDED: +1. Specific system noise characteristics +2. Target frequency range +3. Required attenuation levels +4. Surrounding circuit topology +5. Signal source impedance +6. Loading conditions + +PRIORITY ACTION ITEMS: +[High] Confirm filter performance matches system requirements +[Medium] Validate component tolerances +[Low] Consider alternative filter topologies if needed ==================== top.complete_circuit0.power_section0.voltage_regulator0 ==================== -Executive Summary: -This is a basic linear voltage regulator circuit using an LM7805 to provide a regulated 5V output with input/output capacitive filtering. The design represents a standard implementation but has several areas requiring attention. - -Critical Findings Summary: -1. SEVERITY: High -CATEGORY: Design -DESCRIPTION: Input voltage range unspecified -IMPACT: Could lead to excessive power dissipation -RECOMMENDATION: Specify input voltage range and verify thermal management - -2. SEVERITY: Medium -CATEGORY: Performance -DESCRIPTION: Capacitor values may be insufficient for stability -IMPACT: Potential oscillation under certain load conditions -RECOMMENDATION: Increase output capacitor to 22µF minimum - -Detailed Analysis: - -Power Section Analysis: -- LM7805 provides regulated +5V ±4% output -- Maximum current capability: 1.5A -- Dropout voltage: 2V typical -- Power dissipation (worst case) = (Vin - 5V) × Iout -- Thermal resistance junction-to-ambient: 65°C/W (TO-220 without heatsink) - -Capacitor Configuration: -- Input capacitor (C2): 10µF -- Output capacitor (C3): 10µF -- ESR requirements not specified -- Minimum recommended output capacitance: 22µF per datasheet -- Current design may experience stability issues - -System Performance: -- Load regulation: 0.1% typical -- Line regulation: 0.02%/V typical -- Ripple rejection: 78dB typical at 120Hz -- Output noise: 40µV typical (10Hz to 100kHz) - -Critical Issues: - -1. Thermal Management: -SEVERITY: High -- Maximum junction temperature: 125°C -- Thermal protection activates at 165°C -- Heatsink requirements cannot be calculated without input voltage specification - -2. Stability Concerns: -SEVERITY: Medium -- Current output capacitance (10µF) below recommended minimum -- ESR specifications critical for stability -- Phase margin potentially compromised - -Required Action Items: -1. Specify input voltage range -2. Increase C3 to minimum 22µF -3. Add heatsink calculations based on input voltage -4. Specify ESR requirements for capacitors -5. Add reverse voltage protection -6. Consider adding input/output protection - -Additional Information Needed: -1. Input voltage range specification -2. Maximum load current requirement -3. Operating temperature range -4. PCB thermal characteristics -5. Load transient requirements -6. EMI/EMC requirements -7. Expected ambient temperature -8. Required MTBF -9. Environmental conditions -10. Cost constraints - -Recommendations: -1. Add reverse polarity protection diode -2. Include input transient protection -3. Add output short circuit protection -4. Increase output capacitance to 22µF minimum -5. Consider adding status LED -6. Add test points for voltage measurements -7. Consider switching regulator alternative for better efficiency - -The current design represents a functional but basic implementation that requires additional specification and protection features for robust operation in a practical application. +EXECUTIVE SUMMARY: +The analyzed circuit is a linear voltage regulator subsystem utilizing an LM7805 voltage regulator with input and output capacitive filtering. + +CRITICAL FINDINGS SUMMARY: +- Voltage Regulation Subsystem +- Standard Linear Voltage Regulation Topology +- 5V Fixed Output Voltage Configuration + +DETAILED SUBCIRCUIT ANALYSIS: + +1. Voltage Regulator (U1: LM7805_TO220): +- Standard Linear Voltage Regulator +- Fixed 5V Output Voltage +- TO-220 Package Indicates Power Handling Capability +- Typical Dropout Voltage: Approximately 2V +- Maximum Input Voltage: Typically 35V +- Recommended Input Voltage Range: 7-20V + +SEVERITY: Low +CATEGORY: Power Distribution +SUBCIRCUIT: Voltage Regulation +DESCRIPTION: Standard Linear Voltage Regulator Implementation +IMPACT: Provides Stable 5V Power Supply +RECOMMENDATION: Consider Adding: +- Heatsink for Thermal Management +- Input/Output Protection Diodes +- Overcurrent Protection + +2. Capacitor Configuration (C2, C3): +- Dual 10uF Capacitors +- SMD 0805 Footprint +- Input and Output Filtering +- Provides Noise Suppression and Stability + +SEVERITY: Low +CATEGORY: Power Integrity +SUBCIRCUIT: Decoupling +DESCRIPTION: Standard Capacitive Filtering +RECOMMENDATIONS: +- Add Ceramic Bypass Capacitors (0.1uF) +- Consider Capacitor ESR Characteristics +- Verify Capacitor Temperature Coefficients + +SYSTEM-LEVEL ANALYSIS: +- Simple Linear Voltage Regulation Topology +- Suitable for Low to Medium Power Applications +- Limited Efficiency (Linear Regulators Dissipate Excess Power as Heat) + +POWER PERFORMANCE ESTIMATES: +- Maximum Power Output: Approximately 1W +- Typical Efficiency: 30-50% +- Power Dissipation: Dependent on Input/Output Voltage Differential + +CROSS-CUTTING CONCERNS: +- Thermal Management Critical +- Voltage Drop Considerations +- Noise Susceptibility + +RECOMMENDATIONS SUMMARY: +1. Add Ceramic Bypass Capacitors +2. Implement Thermal Management +3. Consider Switching Regulator for Higher Efficiency +4. Add Overcurrent Protection +5. Implement Input Voltage Filtering + +REQUIRED ACTION ITEMS: +[Priority: High] +- Thermal Analysis +- Capacitor Selection Verification +- Voltage Range Confirmation +- Protection Circuit Implementation + +ADDITIONAL INFORMATION NEEDED: +1. Exact Input Voltage Range +2. Maximum Expected Load Current +3. Ambient Temperature Operating Range +4. Specific Application Context +5. Power Supply Source Characteristics + +The analysis provides a comprehensive overview of the voltage regulation subsystem, highlighting potential improvements and considerations for robust implementation. From 6803f4e006e19ee902184c1bfba7b8acf95dae8b Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 12:27:51 -0800 Subject: [PATCH 16/73] refactor to use openrouter --- src/skidl/circuit.py | 38 ++++--- src/skidl/circuit_analyzer.py | 182 ++++++++-------------------------- 2 files changed, 65 insertions(+), 155 deletions(-) diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 6bcb9e11d..437062183 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -192,6 +192,17 @@ def activate(self, name, tag): self.hierarchy = self.hierarchy + HIER_SEP + name + str(tag) self.add_hierarchical_name(self.hierarchy) + # Store subcircuit docstring if available + import inspect + frame = inspect.currentframe() + try: + # Go up 2 frames to get to the subcircuit function + subcircuit_func = frame.f_back.f_back.f_locals.get('f') + if subcircuit_func and subcircuit_func.__doc__: + self.subcircuit_docs[self.hierarchy] = subcircuit_func.__doc__.strip() + finally: + del frame # Avoid reference cycles + # Setup some globals needed in this context. builtins.default_circuit = self builtins.NC = self.NC # pylint: disable=undefined-variable @@ -1260,17 +1271,6 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip return "\n".join(circuit_info) - def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt"): - """ - Analyze the circuit using LLM, starting from top level and including all subcircuits. - """ - return self._analyze_with_llm( - hierarchy=self.hierarchy, - depth=None, # Analyze all levels - api_key=api_key, - output_file=output_file - ) - def analyze_with_llm(self, hierarchy=None, depth=None, api_key=None, output_file="circuit_llm_analysis.txt"): """ Analyze the circuit using LLM. @@ -1296,6 +1296,20 @@ def analyze_with_llm(self, hierarchy=None, depth=None, api_key=None, output_file def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_analysis.txt"): """ Analyze each subcircuit separately using LLM. + + This method analyzes each subcircuit individually with depth=1 to focus on + the specific functionality of that subcircuit level. + + Args: + api_key: API key for the LLM service + output_file: File to save consolidated analysis results + + Returns: + Dictionary containing: + - success: Overall success status + - subcircuits: Dict of analysis results for each subcircuit + - total_time_seconds: Total analysis time + - total_tokens: Total tokens used """ results = { "success": True, @@ -1315,7 +1329,7 @@ def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_an # Analyze just this level with depth=1 sub_results = self.analyze_with_llm( hierarchy=hier, - depth=1, + depth=1, # Focus on just this subcircuit level api_key=api_key, output_file=None # Don't write individual files ) diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index fb12c760b..c96146f94 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -1,146 +1,17 @@ -"""Module for circuit analysis using LLMs.""" +"""Module for circuit analysis using LLMs through OpenRouter.""" from typing import Dict, Optional from datetime import datetime import time import os -import json import requests -from typing import Dict, Any, List, Optional, Union - -class LLMInterface: - """ - A flexible interface for interacting with various LLM providers. - Supports custom prompts, logging, and different response formats. - """ - - def __init__(self, - provider: str = "openrouter", - model: str = None, - api_key: str = None, - base_url: str = None, - logging_enabled: bool = True, - max_length: int = 8000, - **kwargs): - """ - Initialize LLM Interface - """ - self.provider = provider.lower() - self.model = model or self._get_default_model() - # Support both OPENROUTER_API_KEY and ANTHROPIC_API_KEY for backward compatibility - self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") or os.getenv("ANTHROPIC_API_KEY") - self.base_url = base_url or self._get_default_base_url() - self.max_length = max_length - self.kwargs = kwargs - - def _get_default_model(self) -> str: - """Get default model based on provider""" - defaults = { - "openrouter": "anthropic/claude-3.5-haiku", - "ollama": "llama3.2", - "anthropic": "claude-3-opus-20240229" - } - return defaults.get(self.provider, "") - - def _get_default_base_url(self) -> str: - """Get default base URL based on provider""" - defaults = { - "openrouter": "https://openrouter.ai/api/v1", - "ollama": "http://localhost:11434", - "anthropic": "https://api.anthropic.com/v1" - } - return defaults.get(self.provider, "") - - def process(self, - messages: List[Dict[str, str]], - options: Optional[Dict[str, Any]] = None, - **kwargs) -> Dict[str, Any]: - """Process a request through the LLM""" - if not messages: - raise ValueError("Messages list cannot be empty") - - # Apply content length limit if specified - if self.max_length: - messages = self._truncate_messages(messages) - - # Merge options with instance kwargs - request_options = {**self.kwargs, **(options or {}), **kwargs} - - # Process based on provider - processor = getattr(self, f"_process_{self.provider}", None) - if not processor: - raise ValueError(f"Unsupported provider: {self.provider}") - - try: - return processor(messages, request_options) - except Exception as e: - raise - - def _truncate_messages(self, messages: List[Dict[str, str]]) -> List[Dict[str, str]]: - """Truncate message content to max_length""" - truncated = [] - for msg in messages: - content = msg.get('content', '') - if len(content) > self.max_length: - msg = dict(msg) - msg['content'] = content[:self.max_length] + "..." - truncated.append(msg) - return truncated - - def _process_openrouter(self, - messages: List[Dict[str, str]], - options: Dict[str, Any]) -> Dict[str, Any]: - """Process request through OpenRouter""" - if not self.api_key: - raise ValueError("API key required for OpenRouter") - - headers = { - "Authorization": f"Bearer {self.api_key}", - "HTTP-Referer": "https://github.com/devbisme/skidl", # Required - "X-Title": "SKiDL Circuit Analyzer", # Optional, shown in OpenRouter dashboard - "Content-Type": "application/json" - } - - data = { - "model": self.model, - "messages": messages, - "temperature": 0.7, - "max_tokens": 8000, - "headers": { # OpenRouter-specific headers in the request body - "HTTP-Referer": "https://github.com/devbisme/skidl", - "X-Title": "SKiDL Circuit Analyzer" - } - } - - url = f"{self.base_url}/chat/completions" - response = requests.post( - url, - headers=headers, - json=data, - timeout=30 # Add timeout to prevent hanging - ) - response.raise_for_status() - return response.json() - - def quick_prompt(self, - content: str, - system_prompt: Optional[str] = None, - temperature: float = 0.7) -> str: - """Simplified interface for quick prompts""" - messages = [] - if system_prompt: - messages.append({"role": "system", "content": system_prompt}) - messages.append({"role": "user", "content": content}) - - response = self.process(messages, {"temperature": temperature}) - return response['choices'][0]['message']['content'] class SkidlCircuitAnalyzer: - """Circuit analyzer using Large Language Models.""" + """Circuit analyzer using Large Language Models through OpenRouter.""" def __init__( self, - model: str = "anthropic/claude-3.5-haiku", + model: str = "anthropic/claude-3-haiku", api_key: Optional[str] = None, custom_prompt: Optional[str] = None, analysis_flags: Optional[Dict[str, bool]] = None, @@ -150,18 +21,22 @@ def __init__( Initialize the circuit analyzer. Args: - model: Name of the LLM model to use - api_key: API key for the LLM service + model: Name of the OpenRouter model to use + api_key: OpenRouter API key (or set OPENROUTER_API_KEY env var) custom_prompt: Additional custom prompt template to append analysis_flags: Dict of analysis sections to enable/disable **kwargs: Additional configuration options """ - self.llm = LLMInterface( - provider="openrouter", - model=model, - api_key=api_key, - **kwargs - ) + # Check for OPENROUTER_API_KEY environment variable + self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") + if not self.api_key: + raise ValueError( + "OpenRouter API key required. Either:\n" + "1. Set OPENROUTER_API_KEY environment variable\n" + "2. Pass api_key parameter to analyze_with_llm" + ) + + self.model = model self.custom_prompt = custom_prompt self.analysis_flags = analysis_flags or { @@ -234,7 +109,7 @@ def analyze_circuit( start_time = time.time() if verbose: - print(f"\n=== Starting Circuit Analysis with {self.llm.model} ===") + print(f"\n=== Starting Circuit Analysis with {self.model} ===") try: # Generate the analysis prompt @@ -243,9 +118,30 @@ def analyze_circuit( if verbose: print("\nGenerating analysis...") - # Get analysis from LLM + # Get analysis from OpenRouter request_start = time.time() - analysis_text = self.llm.quick_prompt(prompt) + headers = { + "Authorization": f"Bearer {self.api_key}", + "HTTP-Referer": "https://github.com/devbisme/skidl", + "X-Title": "SKiDL Circuit Analyzer" + } + + data = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.7, + "max_tokens": 4000, + } + + response = requests.post( + "https://openrouter.ai/api/v1/chat/completions", + headers=headers, + json=data, + timeout=30 + ) + response.raise_for_status() + + analysis_text = response.json()["choices"][0]["message"]["content"] request_time = time.time() - request_start # Prepare results From b4a22be8c20256fbe1e7fd6cae3ad1aa454ff4a4 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 12:39:43 -0800 Subject: [PATCH 17/73] refactor llm test script to include more info --- setup.py | 2 +- skidl_llm_test.py | 227 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 220 insertions(+), 9 deletions(-) diff --git a/setup.py b/setup.py index 674979faf..94471ec17 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ #'PySpice; python_version >= "3.0"', "graphviz", "deprecation", - "anthropic >= 0.18.0", + "requests >= 2.31.0", ] test_requirements = [ diff --git a/skidl_llm_test.py b/skidl_llm_test.py index ab1ed30bf..e757dfd76 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -3,9 +3,36 @@ @subcircuit def rc_filter(inp, outp, gnd): - """RC low-pass filter""" + """RC low-pass filter subcircuit. + + A simple first-order low-pass filter consisting of a series resistor + and shunt capacitor. Used for smoothing power supply ripple and noise. + + Args: + inp (Net): Input net connection + outp (Net): Output net connection + gnd (Net): Ground reference net + + Components: + - 10K SMD resistor (R0805) + - 0.1uF SMD capacitor (C0805) + + Cutoff frequency: ~160Hz (f = 1/(2*pi*R*C)) + """ r = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='10K') c = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='0.1uF') + + # Add fields for BOM and documentation + r.fields['manufacturer'] = 'Yageo' + r.fields['mpn'] = 'RC0805FR-0710KL' + r.fields['tolerance'] = '1%' + r.fields['power'] = '0.125W' + + c.fields['manufacturer'] = 'Murata' + c.fields['mpn'] = 'GRM21BR71H104KA01L' + c.fields['voltage'] = '50V' + c.fields['tolerance'] = '10%' + inp += r[1] r[2] += c[1] c[2] += gnd @@ -13,7 +40,25 @@ def rc_filter(inp, outp, gnd): @subcircuit def input_protection(inp, outp, gnd): - """Input protection and bulk capacitance""" + """Input protection and bulk capacitance subcircuit. + + Provides reverse polarity protection using a diode and input + stabilization using a bulk capacitor. Essential for protecting + downstream components and maintaining stable input voltage. + + Args: + inp (Net): Raw input voltage net + outp (Net): Protected output net + gnd (Net): Ground reference net + + Components: + - 1N4148W SMD protection diode (SOD-123) + - 100uF through-hole bulk capacitor + + Protection features: + - Reverse polarity protection up to max diode rating + - Input smoothing with large bulk capacitance + """ d_protect = Part("Device", 'D', footprint='Diode_SMD:D_SOD-123', value='1N4148W') @@ -21,6 +66,18 @@ def input_protection(inp, outp, gnd): footprint='Capacitor_THT:CP_Radial_D10.0mm_P5.00mm', value='100uF') + # Add fields for BOM and documentation + d_protect.fields['manufacturer'] = 'ON Semiconductor' + d_protect.fields['mpn'] = '1N4148W-7-F' + d_protect.fields['voltage'] = '75V' + d_protect.fields['current'] = '150mA' + + c_bulk.fields['manufacturer'] = 'Panasonic' + c_bulk.fields['mpn'] = 'EEU-FR1H101' + c_bulk.fields['voltage'] = '50V' + c_bulk.fields['tolerance'] = '20%' + c_bulk.fields['lifetime'] = '2000h' + inp += d_protect['A'] outp += d_protect['K'] inp += c_bulk[1] @@ -28,11 +85,49 @@ def input_protection(inp, outp, gnd): @subcircuit def voltage_regulator(inp, outp, gnd): - """5V voltage regulator with decoupling caps""" + """5V voltage regulator with decoupling caps subcircuit. + + Linear voltage regulation section using LM7805 with input/output + decoupling capacitors for stable operation. Provides regulated + 5V output from higher input voltage. + + Args: + inp (Net): Input voltage net (7-35V) + outp (Net): Regulated 5V output net + gnd (Net): Ground reference net + + Components: + - LM7805 5V linear regulator (TO-220) + - 10uF input decoupling cap (C0805) + - 10uF output decoupling cap (C0805) + + Specifications: + - Output voltage: 5V ±4% + - Max input voltage: 35V + - Dropout voltage: ~2V + - Max current: 1A + """ reg = Part("Regulator_Linear", "LM7805_TO220", footprint="Package_TO_SOT_THT:TO-220-3_Vertical") cin = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='10uF') cout = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='10uF') + + # Add fields for BOM and documentation + reg.fields['manufacturer'] = 'Texas Instruments' + reg.fields['mpn'] = 'LM7805CT' + reg.fields['thermal_resistance'] = '5°C/W' + reg.fields['max_junction_temp'] = '125°C' + + cin.fields['manufacturer'] = 'Samsung' + cin.fields['mpn'] = 'CL21A106KPFNNNE' + cin.fields['voltage'] = '10V' + cin.fields['tolerance'] = '10%' + + cout.fields['manufacturer'] = 'Samsung' + cout.fields['mpn'] = 'CL21A106KPFNNNE' + cout.fields['voltage'] = '10V' + cout.fields['tolerance'] = '10%' + inp += cin[1], reg['VI'] cin[2] += gnd reg['GND'] += gnd @@ -41,9 +136,39 @@ def voltage_regulator(inp, outp, gnd): @subcircuit def voltage_divider(inp, outp, gnd): - """Basic voltage divider subcircuit""" + """Basic voltage divider subcircuit. + + Resistive voltage divider network that scales down input voltage + by a factor of 1/3 (1K:500Ω ratio). + + Args: + inp (Net): Input voltage net + outp (Net): Divided output voltage net + gnd (Net): Ground reference net + + Components: + - 1K upper resistor (R0805) + - 500Ω lower resistor (R0805) + + Characteristics: + - Division ratio: 1/3 + - Output impedance: ~333Ω + - Power dissipation: V²/1500Ω + """ r1 = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='1K') r2 = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='500') + + # Add fields for BOM and documentation + r1.fields['manufacturer'] = 'Yageo' + r1.fields['mpn'] = 'RC0805FR-071KL' + r1.fields['tolerance'] = '1%' + r1.fields['power'] = '0.125W' + + r2.fields['manufacturer'] = 'Yageo' + r2.fields['mpn'] = 'RC0805FR-07499RL' + r2.fields['tolerance'] = '1%' + r2.fields['power'] = '0.125W' + inp += r1[1] r1[2] += r2[1] r2[2] += gnd @@ -51,16 +176,58 @@ def voltage_divider(inp, outp, gnd): @subcircuit def output_termination(inp, gnd): - """Output termination resistor""" + """Output termination resistor subcircuit. + + High-value pull-down resistor for output termination and + defined state when unloaded. + + Args: + inp (Net): Input net to terminate + gnd (Net): Ground reference net + + Components: + - 100K termination resistor (R0805) + + Characteristics: + - Light loading (10µA @ 1V) + - Defined output state when floating + """ r_term = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='100K') + + # Add fields for BOM and documentation + r_term.fields['manufacturer'] = 'Yageo' + r_term.fields['mpn'] = 'RC0805FR-07100KL' + r_term.fields['tolerance'] = '1%' + r_term.fields['power'] = '0.125W' + inp += r_term[1] gnd += r_term[2] @subcircuit def power_section(raw_in, reg_out, filt_out, gnd): - """Power section with regulation and filtering""" + """Power section with regulation and filtering subcircuit. + + Complete power supply section combining input protection, + voltage regulation, and output filtering stages. + + Args: + raw_in (Net): Raw input voltage net + reg_out (Net): Regulated 5V output net + filt_out (Net): Filtered final output net + gnd (Net): Ground reference net + + Subsections: + 1. Input protection with bulk capacitance + 2. 5V voltage regulation + 3. RC output filtering + + Characteristics: + - Protected against reverse polarity + - Regulated 5V output + - Filtered output with reduced ripple + """ protected_in = Net() input_protection(raw_in, protected_in, gnd) voltage_regulator(protected_in, reg_out, gnd) @@ -68,7 +235,26 @@ def power_section(raw_in, reg_out, filt_out, gnd): @subcircuit def double_divider(inp, outp, gnd): - """Two voltage dividers in series with termination""" + """Two voltage dividers in series with termination subcircuit. + + Cascaded voltage divider network with output termination, + providing approximately 1/9 division ratio. + + Args: + inp (Net): Input voltage net + outp (Net): Final divided output net + gnd (Net): Ground reference net + + Subsections: + 1. First 1/3 voltage divider + 2. Second 1/3 voltage divider + 3. Output termination + + Characteristics: + - Overall division ratio: ~1/9 + - Cascaded divider stages + - Terminated output + """ mid = Net() voltage_divider(inp, mid, gnd) voltage_divider(mid, outp, gnd) @@ -76,7 +262,26 @@ def double_divider(inp, outp, gnd): @subcircuit def complete_circuit(): - """Top level circuit connecting all subcircuits""" + """Top level circuit connecting all subcircuits. + + Complete circuit implementation combining power supply and + signal conditioning sections. + + Circuit Flow: + 1. Raw input → Protected input + 2. Protected → Regulated 5V + 3. Regulated → Filtered + 4. Filtered → Divided output + + Major Sections: + - Power section (protection, regulation, filtering) + - Signal conditioning (cascaded dividers) + + Characteristics: + - Protected and regulated power + - Filtered and divided output + - Multiple conditioning stages + """ vin = Net() vreg = Net() vfilt = Net() @@ -89,6 +294,12 @@ def complete_circuit(): # Create the complete circuit complete_circuit() +# Print docstrings for each subcircuit before analysis +print("\nSubcircuit Docstrings:") +for name, doc in default_circuit.subcircuit_docs.items(): + print(f"\n{name}:") + print(doc) + # Analyze each subcircuit separately using the new function results = default_circuit.analyze_subcircuits_with_llm( api_key=os.getenv("OPENROUTER_API_KEY"), From 9ff36d0b71087725d2bef125e15eee63558434b1 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 12:43:57 -0800 Subject: [PATCH 18/73] remove unneeded file --- subcircuits_analysis.txt | 368 --------------------------------------- 1 file changed, 368 deletions(-) delete mode 100644 subcircuits_analysis.txt diff --git a/subcircuits_analysis.txt b/subcircuits_analysis.txt deleted file mode 100644 index bf1898622..000000000 --- a/subcircuits_analysis.txt +++ /dev/null @@ -1,368 +0,0 @@ -=== Subcircuits Analysis === - - -==================== top.complete_circuit0.double_divider0.output_termination0 ==================== -EXECUTIVE SUMMARY: -Limited circuit information prevents comprehensive analysis. The provided description focuses on a single resistor (R6) in a termination subcircuit, requiring additional context for meaningful evaluation. - -CRITICAL FINDINGS SUMMARY: -- Insufficient data for complete system analysis -- Single component (R6) provides minimal insight -- Termination network details are incomplete - -COMPONENT ANALYSIS: R6 -- Resistance Value: 100 kΩ -- Footprint: 0805 SMD -- Connectivity: Connected between nodes N$4 and N$5 - -SIGNAL TERMINATION PRELIMINARY ASSESSMENT: -SEVERITY: Medium -CATEGORY: Signal Integrity -DESCRIPTION: Potential output termination resistance network -POTENTIAL FUNCTIONS: -1. Impedance matching -2. Signal reflection reduction -3. Noise suppression - -RECOMMENDATIONS: -1. Obtain complete circuit schematic -2. Verify termination network topology -3. Confirm signal characteristics and frequency range -4. Validate impedance matching requirements - -ADDITIONAL INFORMATION NEEDED: -- Full circuit schematic -- Signal frequency and type -- Transmission line characteristics -- Adjacent circuit component details -- Operating voltage range -- Specific application context - -Without additional context, a comprehensive analysis is not possible. The single resistor suggests an output termination network, but precise characterization requires more information about the signal path, frequency, and system requirements. - -==================== top.complete_circuit0.double_divider0.voltage_divider0 ==================== -EXECUTIVE SUMMARY: -The provided circuit represents a voltage divider subcircuit with two resistive elements (R2 and R3) configured in series. Preliminary analysis reveals a basic voltage scaling network with potential signal conditioning or reference voltage generation functionality. - -CRITICAL FINDINGS SUMMARY: -1. Voltage Divider Configuration Detected -2. Resistor Values: R2 = 1kΩ, R3 = 500Ω -3. Potential Signal Conditioning Application - -DETAILED SUBCIRCUIT ANALYSIS: - -Voltage Divider Characteristics: -- Total Resistance: 1.5kΩ -- Voltage Division Ratio Calculation: - Vout = Vin * (R3 / (R2 + R3)) - Vout = Vin * (500 / (1000 + 500)) - Vout = Vin * (500 / 1500) - Vout = Vin * 0.333 (33.3% voltage scaling) - -SIGNAL INTEGRITY ANALYSIS: -- Resistance Tolerance Impact: Assuming ±5% resistor tolerance -- Potential Voltage Variation: ±1.65% of output voltage -- Node Connections: N$3, N$10, N$5 identified - -POWER DISTRIBUTION ANALYSIS: -- Power Dissipation Calculation: - P = (Vin)² / R_total - Assumes maximum operating voltage and resistance - -RECOMMENDATIONS: -1. Verify precise voltage scaling requirements -2. Confirm resistor tolerance meets design specifications -3. Validate thermal performance under expected load conditions -4. Implement precision resistors if high accuracy is critical - -ADDITIONAL INFORMATION NEEDED: -1. Input voltage range -2. Target application context -3. Required output voltage precision -4. Operating temperature range -5. Expected current load - -SEVERITY MATRIX: -- Design Complexity: Low -- Performance Risk: Medium -- Implementation Difficulty: Low - -Detailed technical recommendations and comprehensive system-level analysis require additional contextual information about the broader circuit design and specific application requirements. - -==================== top.complete_circuit0.double_divider0.voltage_divider1 ==================== -EXECUTIVE SUMMARY -The analyzed circuit segment represents a voltage divider subcircuit with two resistive elements (R4 and R5) configured in series, intended for voltage scaling or signal conditioning purposes. - -CRITICAL FINDINGS SUMMARY: -1. Voltage Divider Configuration Detected -2. Potential Signal Conditioning Subcircuit -3. Requires Additional Context for Complete Analysis - -DETAILED SUBCIRCUIT ANALYSIS: - -Voltage Divider Characteristics: -- R4: 1kΩ resistor -- R5: 500Ω resistor -- Connection Nodes: N$9 → N$11 → N$5 - -Voltage Division Calculation: -Vout = Vin * (R5 / (R4 + R5)) -Voltage Scaling Factor: Approximately 0.333 (33.3%) - -SIGNAL CHARACTERISTICS: -- Low Impedance Output Path -- Simple Linear Voltage Reduction -- Potential Signal Conditioning Application - -RECOMMENDATIONS: -1. Verify intended voltage scaling requirements -2. Confirm noise immunity and signal integrity -3. Validate thermal performance under expected load conditions - -CRITICAL MISSING INFORMATION: -1. Input voltage range -2. Maximum current requirements -3. Operating temperature specifications -4. Specific circuit application context -5. Full circuit schematic -6. Performance tolerance expectations - -ADDITIONAL INFORMATION NEEDED: -- Complete circuit context -- Specific design requirements -- Input signal characteristics -- Expected operating environment - -Severity: Low-Medium -Confidence Level: Partial (Limited Context Available) - -Detailed analysis requires supplementary circuit documentation for comprehensive evaluation. - -==================== top.complete_circuit0.power_section0.input_protection0 ==================== -EXECUTIVE SUMMARY: -The analyzed circuit represents a basic input protection stage with minimal components, focusing on power supply conditioning and protection. - -CRITICAL FINDINGS SUMMARY: -- Simple input protection topology -- Potential voltage clamping and filtering capabilities -- Limited protection against transient events - -DETAILED SUBCIRCUIT ANALYSIS: - -Input Protection Subcircuit Components: -1. Capacitor C1 (100uF) - - Electrolytic capacitor - - Radial through-hole mounting - - Potential bulk filtering/decoupling function - - Large capacitance suggests power supply smoothing - -2. Diode D1 (1N4148W) - - Small signal switching diode - - Surface mount SOD-123 package - - Likely used for reverse polarity protection - - Low forward voltage drop (typical 0.7V) - -SYSTEM-LEVEL ANALYSIS: -- Basic input stage protection mechanism -- Rudimentary voltage regulation approach -- Limited transient suppression capabilities - -POWER DISTRIBUTION ANALYSIS: -- Diode introduces ~0.7V voltage drop -- Capacitor provides minimal energy storage -- No explicit voltage regulation - -SIGNAL INTEGRITY CONSIDERATIONS: -- Basic protection against reverse polarity -- Minimal noise suppression -- No explicit EMI/ESD protection - -RECOMMENDATIONS: - -1. Enhanced Protection Strategy -SEVERITY: Medium -RECOMMENDATION: -- Add TVS diode for improved transient protection -- Implement additional RC filtering -- Consider adding voltage regulator - -2. Component Selection -SEVERITY: Low -RECOMMENDATION: -- Replace 1N4148W with more robust protection diode -- Consider Schottky diode for lower voltage drop -- Evaluate capacitor's temperature and lifetime characteristics - -3. Design Robustness -SEVERITY: Medium -RECOMMENDATION: -- Add overcurrent protection -- Implement bidirectional protection -- Consider adding voltage clamp - -ADDITIONAL INFORMATION NEEDED: -1. Voltage input range -2. Maximum expected current -3. Operating environment details -4. Specific application context -5. Power supply characteristics - -PRIORITY ACTION ITEMS: -1. Add transient voltage suppressor -2. Implement bidirectional protection -3. Enhance filtering capabilities -4. Verify component ratings -5. Conduct comprehensive thermal analysis - -The current design represents a minimal input protection approach with significant opportunities for improvement in robustness, reliability, and performance. - -==================== top.complete_circuit0.power_section0.rc_filter0 ==================== -EXECUTIVE SUMMARY: -The provided circuit represents a basic RC filter subcircuit, characterized by minimal components and a simple topology. Initial analysis reveals a low-complexity power conditioning/filtering stage with potential signal conditioning applications. - -CRITICAL FINDINGS SUMMARY: -1. Simple RC Low-Pass Filter Configuration -2. Limited Filtering Capabilities -3. Potential Signal Conditioning Use Case - -DETAILED SUBCIRCUIT ANALYSIS: - -RC Filter Subcircuit Analysis: ------------------------------- -Components: -- Capacitor C4: 0.1µF -- Resistor R1: 10kΩ - -Filter Characteristics Calculation: -- Cutoff Frequency (fc) = 1 / (2π * R * C) -- fc = 1 / (2π * 10,000 * 0.1µF) -- fc ≈ 159.2 kHz - -Key Observations: -SEVERITY: Low -CATEGORY: Signal Conditioning -SUBCIRCUIT: RC Low-Pass Filter -DESCRIPTION: Basic first-order RC filter with moderate high-frequency attenuation -IMPACT: Provides gentle signal conditioning/noise reduction -VERIFICATION: Frequency response measurement -RECOMMENDATION: Consider application-specific requirements - -Performance Parameters: -- Attenuation Rate: -20 dB/decade -- Filter Order: First-Order -- Passband: DC to ~159 kHz -- Rolloff: Gradual - -SYSTEM-LEVEL ANALYSIS: - -Potential Applications: -1. Power supply noise reduction -2. Analog signal conditioning -3. Basic filtering for sensor interfaces -4. EMI/RFI mitigation - -MANUFACTURING CONSIDERATIONS: -- SMD 0805 footprint supports automated assembly -- Standard component values -- Low complexity reduces manufacturing variability - -RECOMMENDATIONS: -1. Verify specific system noise reduction requirements -2. Validate filter performance against application needs -3. Consider higher-order filtering if sharper rolloff required -4. Evaluate component tolerance impacts - -ADDITIONAL INFORMATION NEEDED: -1. Specific system noise characteristics -2. Target frequency range -3. Required attenuation levels -4. Surrounding circuit topology -5. Signal source impedance -6. Loading conditions - -PRIORITY ACTION ITEMS: -[High] Confirm filter performance matches system requirements -[Medium] Validate component tolerances -[Low] Consider alternative filter topologies if needed - -==================== top.complete_circuit0.power_section0.voltage_regulator0 ==================== -EXECUTIVE SUMMARY: -The analyzed circuit is a linear voltage regulator subsystem utilizing an LM7805 voltage regulator with input and output capacitive filtering. - -CRITICAL FINDINGS SUMMARY: -- Voltage Regulation Subsystem -- Standard Linear Voltage Regulation Topology -- 5V Fixed Output Voltage Configuration - -DETAILED SUBCIRCUIT ANALYSIS: - -1. Voltage Regulator (U1: LM7805_TO220): -- Standard Linear Voltage Regulator -- Fixed 5V Output Voltage -- TO-220 Package Indicates Power Handling Capability -- Typical Dropout Voltage: Approximately 2V -- Maximum Input Voltage: Typically 35V -- Recommended Input Voltage Range: 7-20V - -SEVERITY: Low -CATEGORY: Power Distribution -SUBCIRCUIT: Voltage Regulation -DESCRIPTION: Standard Linear Voltage Regulator Implementation -IMPACT: Provides Stable 5V Power Supply -RECOMMENDATION: Consider Adding: -- Heatsink for Thermal Management -- Input/Output Protection Diodes -- Overcurrent Protection - -2. Capacitor Configuration (C2, C3): -- Dual 10uF Capacitors -- SMD 0805 Footprint -- Input and Output Filtering -- Provides Noise Suppression and Stability - -SEVERITY: Low -CATEGORY: Power Integrity -SUBCIRCUIT: Decoupling -DESCRIPTION: Standard Capacitive Filtering -RECOMMENDATIONS: -- Add Ceramic Bypass Capacitors (0.1uF) -- Consider Capacitor ESR Characteristics -- Verify Capacitor Temperature Coefficients - -SYSTEM-LEVEL ANALYSIS: -- Simple Linear Voltage Regulation Topology -- Suitable for Low to Medium Power Applications -- Limited Efficiency (Linear Regulators Dissipate Excess Power as Heat) - -POWER PERFORMANCE ESTIMATES: -- Maximum Power Output: Approximately 1W -- Typical Efficiency: 30-50% -- Power Dissipation: Dependent on Input/Output Voltage Differential - -CROSS-CUTTING CONCERNS: -- Thermal Management Critical -- Voltage Drop Considerations -- Noise Susceptibility - -RECOMMENDATIONS SUMMARY: -1. Add Ceramic Bypass Capacitors -2. Implement Thermal Management -3. Consider Switching Regulator for Higher Efficiency -4. Add Overcurrent Protection -5. Implement Input Voltage Filtering - -REQUIRED ACTION ITEMS: -[Priority: High] -- Thermal Analysis -- Capacitor Selection Verification -- Voltage Range Confirmation -- Protection Circuit Implementation - -ADDITIONAL INFORMATION NEEDED: -1. Exact Input Voltage Range -2. Maximum Expected Load Current -3. Ambient Temperature Operating Range -4. Specific Application Context -5. Power Supply Source Characteristics - -The analysis provides a comprehensive overview of the voltage regulation subsystem, highlighting potential improvements and considerations for robust implementation. From a72b3f107c399078edc804999a522e410fd06c91 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 14:09:46 -0800 Subject: [PATCH 19/73] increase max tokens --- src/skidl/circuit_analyzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index c96146f94..08cb3c4e8 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -130,7 +130,7 @@ def analyze_circuit( "model": self.model, "messages": [{"role": "user", "content": prompt}], "temperature": 0.7, - "max_tokens": 4000, + "max_tokens": 20000, } response = requests.post( From 1f1fdb80f8fa28689d6e20a3463c35eb1c5f24f5 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 14:30:07 -0800 Subject: [PATCH 20/73] refactor to reduce new functions --- skidl_llm_test.py | 7 +++-- src/skidl/__init__.py | 1 - src/skidl/circuit.py | 59 +++++++++++++++-------------------- src/skidl/circuit_analyzer.py | 2 +- src/skidl/skidl.py | 2 -- 5 files changed, 31 insertions(+), 40 deletions(-) diff --git a/skidl_llm_test.py b/skidl_llm_test.py index e757dfd76..33ea142c7 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -300,10 +300,11 @@ def complete_circuit(): print(f"\n{name}:") print(doc) -# Analyze each subcircuit separately using the new function -results = default_circuit.analyze_subcircuits_with_llm( +# Analyze each subcircuit separately using analyze_with_llm +results = default_circuit.analyze_with_llm( api_key=os.getenv("OPENROUTER_API_KEY"), - output_file="subcircuits_analysis.txt" + output_file="subcircuits_analysis.txt", + analyze_subcircuits=True ) # Print analysis results diff --git a/src/skidl/__init__.py b/src/skidl/__init__.py index 75300ab92..3f6fb0de3 100644 --- a/src/skidl/__init__.py +++ b/src/skidl/__init__.py @@ -60,7 +60,6 @@ lib_search_paths, get_circuit_info, analyze_with_llm, - analyze_subcircuits_with_llm, no_files, reset, get_default_tool, diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 437062183..d6344cd7f 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1271,46 +1271,37 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip return "\n".join(circuit_info) - def analyze_with_llm(self, hierarchy=None, depth=None, api_key=None, output_file="circuit_llm_analysis.txt"): + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False): """ - Analyze the circuit using LLM. + Analyze the circuit using LLM, with options for analyzing the whole circuit or individual subcircuits. Args: + api_key: API key for the LLM service + output_file: File to save analysis results. If analyzing subcircuits, this will contain consolidated results. hierarchy: Starting hierarchy level to analyze. If None, starts from top. depth: How many levels deep to analyze. If None, analyzes all levels. - api_key: API key for the LLM service - output_file: File to save analysis results + analyze_subcircuits: If True, analyzes each subcircuit separately with depth=1. + If False, analyzes from the specified hierarchy and depth. Returns: - Dictionary containing analysis results + If analyze_subcircuits=False: + Dictionary containing single analysis results + If analyze_subcircuits=True: + Dictionary containing: + - success: Overall success status + - subcircuits: Dict of analysis results for each subcircuit + - total_time_seconds: Total analysis time + - total_tokens: Total tokens used """ from .circuit_analyzer import SkidlCircuitAnalyzer - - # Get circuit description - circuit_desc = self.get_circuit_info(hierarchy=hierarchy, depth=depth) - - # Create analyzer and run analysis analyzer = SkidlCircuitAnalyzer(api_key=api_key) - return analyzer.analyze_circuit(circuit_desc, output_file=output_file) - - def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_analysis.txt"): - """ - Analyze each subcircuit separately using LLM. - This method analyzes each subcircuit individually with depth=1 to focus on - the specific functionality of that subcircuit level. + if not analyze_subcircuits: + # Single analysis of specified hierarchy + circuit_desc = self.get_circuit_info(hierarchy=hierarchy, depth=depth) + return analyzer.analyze_circuit(circuit_desc, output_file=output_file) - Args: - api_key: API key for the LLM service - output_file: File to save consolidated analysis results - - Returns: - Dictionary containing: - - success: Overall success status - - subcircuits: Dict of analysis results for each subcircuit - - total_time_seconds: Total analysis time - - total_tokens: Total tokens used - """ + # Analyze each subcircuit separately results = { "success": True, "subcircuits": {}, @@ -1326,13 +1317,15 @@ def analyze_subcircuits_with_llm(self, api_key=None, output_file="subcircuits_an # Analyze each subcircuit for hier in sorted(hierarchies): - # Analyze just this level with depth=1 - sub_results = self.analyze_with_llm( - hierarchy=hier, - depth=1, # Focus on just this subcircuit level - api_key=api_key, + # Get description focused on this subcircuit + circuit_desc = self.get_circuit_info(hierarchy=hier, depth=1) + + # Analyze just this subcircuit + sub_results = analyzer.analyze_circuit( + circuit_desc, output_file=None # Don't write individual files ) + results["subcircuits"][hier] = sub_results results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("response_tokens", 0) diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index 08cb3c4e8..e7eb5f0ff 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -11,7 +11,7 @@ class SkidlCircuitAnalyzer: def __init__( self, - model: str = "anthropic/claude-3-haiku", + model: str = "anthropic/claude-3.5-haiku", api_key: Optional[str] = None, custom_prompt: Optional[str] = None, analysis_flags: Optional[Dict[str, bool]] = None, diff --git a/src/skidl/skidl.py b/src/skidl/skidl.py index e54904faa..b3e87830e 100644 --- a/src/skidl/skidl.py +++ b/src/skidl/skidl.py @@ -27,7 +27,6 @@ "generate_graph", "get_circuit_info", "analyze_with_llm", - "analyze_subcircuits_with_llm", "reset", "backup_parts", "empty_footprint_handler", @@ -74,7 +73,6 @@ no_files = default_circuit.no_files get_circuit_info = default_circuit.get_circuit_info analyze_with_llm = default_circuit.analyze_with_llm -analyze_subcircuits_with_llm = default_circuit.analyze_subcircuits_with_llm empty_footprint_handler = default_empty_footprint_handler From f269c9aaaa901c65769a5d1bb2b76be19ab448e2 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 14:47:39 -0800 Subject: [PATCH 21/73] refactor to simplify --- src/skidl/circuit.py | 31 ++++++--- src/skidl/circuit_analyzer.py | 123 ++++++++++++++++++++++++---------- src/skidl/prompts/__init__.py | 14 ++++ src/skidl/prompts/base.py | 109 ++++++++++++++++++++++++++++++ src/skidl/prompts/sections.py | 122 +++++++++++++++++++++++++++++++++ 5 files changed, 355 insertions(+), 44 deletions(-) create mode 100644 src/skidl/prompts/__init__.py create mode 100644 src/skidl/prompts/base.py create mode 100644 src/skidl/prompts/sections.py diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index d6344cd7f..45ffc370e 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1332,14 +1332,29 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", # Save consolidated results if requested if output_file: + consolidated_text = ["=== Subcircuits Analysis ===\n"] + + for hier, analysis in results["subcircuits"].items(): + consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") + if analysis.get("success", False): + # Include the actual analysis text + analysis_text = analysis.get("analysis", "No analysis available") + consolidated_text.append(analysis_text) + + # Include token usage info + token_info = ( + f"\nTokens used: {analysis.get('total_tokens', 0)} " + f"(Prompt: {analysis.get('prompt_tokens', 0)}, " + f"Completion: {analysis.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + else: + consolidated_text.append( + f"Analysis failed: {analysis.get('error', 'Unknown error')}" + ) + consolidated_text.append("\n") + with open(output_file, "w") as f: - f.write("=== Subcircuits Analysis ===\n\n") - for hier, analysis in results["subcircuits"].items(): - f.write(f"\n{'='*20} {hier} {'='*20}\n") - if analysis.get("success", False): - f.write(analysis["analysis"]) - else: - f.write(f"Analysis failed: {analysis.get('error', 'Unknown error')}") - f.write("\n") + f.write("\n".join(consolidated_text)) return results diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index e7eb5f0ff..cf9c73e43 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -6,15 +6,35 @@ import os import requests +# API configuration +OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" +DEFAULT_MODEL = "anthropic/claude-3.5-haiku" +DEFAULT_TIMEOUT = 30 +DEFAULT_TEMPERATURE = 0.7 +DEFAULT_MAX_TOKENS = 20000 +MAX_RETRIES = 3 + class SkidlCircuitAnalyzer: - """Circuit analyzer using Large Language Models through OpenRouter.""" + """ + Circuit analyzer using Large Language Models through OpenRouter. + + Attributes: + model: Name of the OpenRouter model to use + api_key: OpenRouter API key + custom_prompt: Additional custom prompt template + analysis_flags: Dict of analysis sections to enable/disable + config: Additional configuration options + """ def __init__( self, - model: str = "anthropic/claude-3.5-haiku", + model: str = DEFAULT_MODEL, api_key: Optional[str] = None, custom_prompt: Optional[str] = None, analysis_flags: Optional[Dict[str, bool]] = None, + timeout: int = DEFAULT_TIMEOUT, + temperature: float = DEFAULT_TEMPERATURE, + max_tokens: int = DEFAULT_MAX_TOKENS, **kwargs ): """ @@ -37,19 +57,38 @@ def __init__( ) self.model = model - + self.timeout = timeout + self.temperature = temperature + self.max_tokens = max_tokens self.custom_prompt = custom_prompt + + # Validate analysis flags against available sections + from .analysis_prompts import ANALYSIS_SECTIONS self.analysis_flags = analysis_flags or { - "system_overview": True, - "design_review": True, - "power_analysis": True, - "signal_integrity": True, - "thermal_analysis": True, - "noise_analysis": True, - "testing_verification": True + section: True for section in ANALYSIS_SECTIONS.keys() } + invalid_sections = set(self.analysis_flags.keys()) - set(ANALYSIS_SECTIONS.keys()) + if invalid_sections: + raise ValueError(f"Invalid analysis sections: {invalid_sections}") + self.config = kwargs + def _save_analysis(self, output_file: str, analysis_text: str, verbose: bool = True) -> None: + """ + Save analysis results to a file. + + Args: + output_file: Path to save the analysis + analysis_text: Analysis content to save + verbose: Whether to print progress messages + """ + if verbose: + print(f"\nSaving analysis to {output_file}...") + with open(output_file, "w") as f: + f.write(analysis_text) + if verbose: + print("Analysis saved successfully") + def _generate_analysis_prompt(self, circuit_description: str) -> str: """ Generate the complete analysis prompt. @@ -60,7 +99,7 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: Returns: Complete prompt string for the LLM """ - from .analysis_prompts import ANALYSIS_SECTIONS, BASE_ANALYSIS_PROMPT + from .prompts import get_base_prompt, ANALYSIS_SECTIONS # Build enabled analysis sections enabled_sections = [] @@ -70,8 +109,8 @@ def _generate_analysis_prompt(self, circuit_description: str) -> str: analysis_sections = "\n".join(enabled_sections) - # Format base prompt with circuit description and sections - prompt = BASE_ANALYSIS_PROMPT.format( + # Generate complete prompt using base template + prompt = get_base_prompt( circuit_description=circuit_description, analysis_sections=analysis_sections ) @@ -118,7 +157,7 @@ def analyze_circuit( if verbose: print("\nGenerating analysis...") - # Get analysis from OpenRouter + # Get analysis from OpenRouter with retries request_start = time.time() headers = { "Authorization": f"Bearer {self.api_key}", @@ -129,22 +168,36 @@ def analyze_circuit( data = { "model": self.model, "messages": [{"role": "user", "content": prompt}], - "temperature": 0.7, - "max_tokens": 20000, + "temperature": self.temperature, + "max_tokens": self.max_tokens, } - response = requests.post( - "https://openrouter.ai/api/v1/chat/completions", - headers=headers, - json=data, - timeout=30 - ) - response.raise_for_status() - - analysis_text = response.json()["choices"][0]["message"]["content"] - request_time = time.time() - request_start - - # Prepare results + # Implement retries with exponential backoff + for attempt in range(MAX_RETRIES): + try: + response = requests.post( + OPENROUTER_API_URL, + headers=headers, + json=data, + timeout=self.timeout + ) + response.raise_for_status() + response_json = response.json() + analysis_text = response_json["choices"][0]["message"]["content"] + request_time = time.time() - request_start + + # Track token usage + usage = response_json.get("usage", {}) + prompt_tokens = usage.get("prompt_tokens", 0) + completion_tokens = usage.get("completion_tokens", 0) + total_tokens = usage.get("total_tokens", 0) + break + except requests.exceptions.RequestException as e: + if attempt == MAX_RETRIES - 1: # Last attempt + raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") + time.sleep(2 ** attempt) # Exponential backoff + + # Prepare results with token usage results = { "success": True, "analysis": analysis_text, @@ -153,17 +206,15 @@ def analyze_circuit( "total_time_seconds": time.time() - start_time, "enabled_analyses": [ k for k, v in self.analysis_flags.items() if v - ] + ], + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens } - # Save to file if requested + # Extract file operations to separate method if output_file: - if verbose: - print(f"\nSaving analysis to {output_file}...") - with open(output_file, "w") as f: - f.write(analysis_text) - if verbose: - print("Analysis saved successfully") + self._save_analysis(output_file, analysis_text, verbose) if verbose: print(f"\n=== Analysis completed in {results['total_time_seconds']:.2f} seconds ===") diff --git a/src/skidl/prompts/__init__.py b/src/skidl/prompts/__init__.py new file mode 100644 index 000000000..ce23eba59 --- /dev/null +++ b/src/skidl/prompts/__init__.py @@ -0,0 +1,14 @@ +"""Circuit analysis prompt templates. + +This package provides templates for generating circuit analysis prompts. +The prompts are organized into: +1. Base prompt structure and methodology +2. Individual analysis section templates +""" + +from .base import get_base_prompt +from .sections import ANALYSIS_SECTIONS + +__version__ = "1.0.0" + +__all__ = ["get_base_prompt", "ANALYSIS_SECTIONS"] diff --git a/src/skidl/prompts/base.py b/src/skidl/prompts/base.py new file mode 100644 index 000000000..1dbf4a2eb --- /dev/null +++ b/src/skidl/prompts/base.py @@ -0,0 +1,109 @@ +"""Base prompt template for circuit analysis.""" + +__version__ = "1.0.0" + +BASE_METHODOLOGY = """ +ANALYSIS METHODOLOGY: +1. Begin analysis immediately with available information +2. After completing analysis, identify any critical missing information needed for deeper insights +3. Begin with subcircuit identification and individual analysis +4. Analyze interactions between subcircuits +5. Evaluate system-level performance and integration +6. Assess manufacturing and practical implementation considerations +""" + +SECTION_REQUIREMENTS = """ +For each analysis section: +1. Analyze with available information first +2. Start with critical missing information identification +3. Provide detailed technical analysis with calculations +4. Include specific numerical criteria and measurements +5. Reference relevant industry standards +6. Provide concrete recommendations +7. Prioritize findings by severity +8. Include specific action items +""" + +ISSUE_FORMAT = """ +For each identified issue: +SEVERITY: (Critical/High/Medium/Low) +CATEGORY: (Design/Performance/Safety/Manufacturing/etc.) +SUBCIRCUIT: Affected subcircuit or system level +DESCRIPTION: Detailed issue description +IMPACT: Quantified impact on system performance +VERIFICATION: How to verify the issue exists +RECOMMENDATION: Specific action items with justification +STANDARDS: Applicable industry standards +TRADE-OFFS: Impact of proposed changes +PRIORITY: Implementation priority level +""" + +SPECIAL_REQUIREMENTS = """ +Special Requirements: +- Analyze each subcircuit completely before moving to system-level analysis +- Provide specific component recommendations where applicable +- Include calculations and formulas used in analysis +- Reference specific standards and requirements +- Consider worst-case scenarios +- Evaluate corner cases +- Assess impact of component variations +- Consider environmental effects +- Evaluate aging effects +- Assess maintenance requirements +""" + +OUTPUT_FORMAT = """ +Output Format: +1. Executive Summary +2. Critical Findings Summary +3. Detailed Subcircuit Analysis (one section per subcircuit) +4. System-Level Analysis +5. Cross-Cutting Concerns +6. Recommendations Summary +7. Required Action Items (prioritized) +8. Additional Information Needed +""" + +IMPORTANT_INSTRUCTIONS = """ +IMPORTANT INSTRUCTIONS: +- Start analysis immediately - do not acknowledge the request or state that you will analyze +- Be specific and quantitative where possible +- Include calculations and methodology +- Reference specific standards +- Provide actionable recommendations +- Consider practical implementation +- Evaluate cost implications +- Assess manufacturing feasibility +- Consider maintenance requirements +""" + +def get_base_prompt(circuit_description: str, analysis_sections: str) -> str: + """ + Generate the complete base analysis prompt. + + Args: + circuit_description: Description of the circuit to analyze + analysis_sections: String containing enabled analysis sections + + Returns: + Complete formatted base prompt + """ + return f""" +You are an expert electronics engineer. Analyze the following circuit design immediately and provide actionable insights. Do not acknowledge the request or promise to analyze - proceed directly with your analysis. + +Circuit Description: +{circuit_description} + +{BASE_METHODOLOGY} + +REQUIRED ANALYSIS SECTIONS: +{analysis_sections} + +{SECTION_REQUIREMENTS} +{ISSUE_FORMAT} +{SPECIAL_REQUIREMENTS} +{OUTPUT_FORMAT} +{IMPORTANT_INSTRUCTIONS} + +After completing your analysis, if additional information would enable deeper insights, list specific questions in a separate section titled 'Additional Information Needed' at the end. +""".strip() diff --git a/src/skidl/prompts/sections.py b/src/skidl/prompts/sections.py new file mode 100644 index 000000000..c8211e6b7 --- /dev/null +++ b/src/skidl/prompts/sections.py @@ -0,0 +1,122 @@ +"""Analysis section prompt templates.""" + +from typing import Dict + +__version__ = "1.0.0" + +SYSTEM_OVERVIEW: str = """ +0. System-Level Analysis: +- Comprehensive system architecture review +- Interface analysis between major blocks +- System-level timing and synchronization +- Resource allocation and optimization +- System-level failure modes +- Integration challenges +- Performance bottlenecks +- Scalability assessment +""".strip() + +DESIGN_REVIEW: str = """ +1. Comprehensive Design Architecture Review: +- Evaluate overall hierarchical structure +- Assess modularity and reusability +- Interface protocols analysis +- Control path verification +- Design pattern evaluation +- Critical path analysis +- Feedback loop stability +- Clock domain analysis +- Reset strategy review +- State machine verification +- Resource utilization assessment +- Design rule compliance +""".strip() + +POWER_ANALYSIS: str = """ +2. In-depth Power Distribution Analysis: +- Complete power tree mapping +- Voltage drop calculations +- Current distribution analysis +- Power sequencing requirements +- Brownout behavior analysis +- Load transient response +- Power supply rejection ratio +- Efficiency optimization +- Thermal implications +- Battery life calculations (if applicable) +- Power integrity simulation +- Decoupling strategy +- Ground bounce analysis +""".strip() + +SIGNAL_INTEGRITY: str = """ +3. Detailed Signal Integrity Analysis: +- Critical path timing analysis +- Setup/hold time verification +- Clock skew analysis +- Propagation delay calculations +- Cross-talk assessment +- Reflection analysis +- EMI/EMC considerations +- Signal loading effects +- Impedance matching +- Common mode noise rejection +- Ground loop analysis +- Shield effectiveness +""".strip() + +THERMAL_ANALYSIS: str = """ +4. Thermal Performance Analysis: +- Component temperature rise calculations +- Thermal resistance analysis +- Heat spreading patterns +- Cooling requirements +- Thermal gradient mapping +- Hot spot identification +- Thermal cycling effects +- Temperature derating +- Thermal protection mechanisms +- Cooling solution optimization +""".strip() + +NOISE_ANALYSIS: str = """ +5. Comprehensive Noise Analysis: +- Noise source identification +- Noise coupling paths +- Ground noise analysis +- Power supply noise +- Digital switching noise +- RF interference +- Common mode noise +- Differential mode noise +- Shielding effectiveness +- Filter performance +- Noise margin calculations +""".strip() + +TESTING_VERIFICATION: str = """ +6. Testing and Verification Strategy: +- Functional test coverage +- Performance verification +- Environmental testing +- Reliability testing +- Safety verification +- EMC/EMI testing +- Production test strategy +- Self-test capabilities +- Calibration requirements +- Diagnostic capabilities +- Test point access +- Debug interface requirements +""".strip() + +# Dictionary mapping section names to their prompts +ANALYSIS_SECTIONS: Dict[str, str] = { + "system_overview": SYSTEM_OVERVIEW, + "design_review": DESIGN_REVIEW, + "power_analysis": POWER_ANALYSIS, + "signal_integrity": SIGNAL_INTEGRITY, + "thermal_analysis": THERMAL_ANALYSIS, + "noise_analysis": NOISE_ANALYSIS, + "testing_verification": TESTING_VERIFICATION +} From 8ec9da15e71841b0b9e345f57dfe5851e3f6d225 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 15:07:22 -0800 Subject: [PATCH 22/73] refactor --- .gitignore | 2 +- src/skidl/analysis_prompts.py | 2 +- src/skidl/circuit.py | 176 ++++++++++++++++++++++++++++++++++ src/skidl/circuit_analyzer.py | 2 +- 4 files changed, 179 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b127a5f8c..b1d5f56e9 100644 --- a/.gitignore +++ b/.gitignore @@ -187,4 +187,4 @@ uno_r3 *.pkl # Mac stuff -.DS_Store \ No newline at end of file +.DS_Store diff --git a/src/skidl/analysis_prompts.py b/src/skidl/analysis_prompts.py index e52843ea6..036d1ba40 100644 --- a/src/skidl/analysis_prompts.py +++ b/src/skidl/analysis_prompts.py @@ -181,4 +181,4 @@ "thermal_analysis": THERMAL_ANALYSIS_PROMPT, "noise_analysis": NOISE_ANALYSIS_PROMPT, "testing_verification": TESTING_VERIFICATION_PROMPT -} \ No newline at end of file +} diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 45ffc370e..4e2cb7401 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1358,3 +1358,179 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", f.write("\n".join(consolidated_text)) return results + + def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_description.txt"): + """ + Save circuit information to a text file and return the description as a string. + Shows hierarchical structure of the circuit with consolidated parts and connections. + + Args: + hierarchy (str): Starting hierarchy level to analyze. If None, starts from top. + depth (int): How many levels deep to analyze. If None, analyzes all levels. + filename (str): Output filename for the circuit description. + """ + circuit_info = [] + circuit_info.append("Circuit Description:") + circuit_info.append("=" * 40) + + # Get starting hierarchy + start_hier = hierarchy or self.hierarchy + circuit_info.append(f"Circuit Name: {self.name}") + circuit_info.append(f"Starting Hierarchy: {start_hier}") + + # Collect all hierarchies at or below the starting point + hierarchies = set() + for part in self.parts: + if part.hierarchy.startswith(start_hier): + # Check depth constraint if specified + if depth is None or len(part.hierarchy.split('.')) - len(start_hier.split('.')) <= depth: + hierarchies.add(part.hierarchy) + + # Group parts by hierarchy + hierarchy_parts = {} + for part in self.parts: + if part.hierarchy in hierarchies: + if part.hierarchy not in hierarchy_parts: + hierarchy_parts[part.hierarchy] = [] + hierarchy_parts[part.hierarchy].append(part) + + # Get nets and group by hierarchy + distinct_nets = self.get_nets() + net_hierarchies = {} + for net in distinct_nets: + net_hier_connections = {} + for pin in net.pins: + if pin.part.hierarchy in hierarchies: + hier = pin.part.hierarchy + if hier not in net_hier_connections: + net_hier_connections[hier] = [] + net_hier_connections[hier].append(pin) + + for hier in net_hier_connections: + if hier not in net_hierarchies: + net_hierarchies[hier] = [] + net_hierarchies[hier].append((net, net_hier_connections)) + + # Print consolidated information for each hierarchy level + first_hierarchy = True + for hier in sorted(hierarchies): + if not first_hierarchy: + circuit_info.append("_" * 53) + else: + first_hierarchy = False + + circuit_info.append(f"Hierarchy Level: {hier}") + + # Add subcircuit docstring if available + if hier in self.subcircuit_docs: + circuit_info.append("\nSubcircuit Documentation:") + circuit_info.append(self.subcircuit_docs[hier]) + circuit_info.append("") + + # Parts in this hierarchy + if hier in hierarchy_parts: + circuit_info.append("Parts:") + for part in sorted(hierarchy_parts[hier], key=lambda p: p.ref): + circuit_info.append(f" Part: {part.ref}") + circuit_info.append(f" Name: {part.name}") + circuit_info.append(f" Value: {part.value}") + circuit_info.append(f" Footprint: {part.footprint}") + # Add part docstring if available + if hasattr(part, 'description'): + circuit_info.append(f" Description: {part.description}") + circuit_info.append(" Pins:") + for pin in part.pins: + net_name = pin.net.name if pin.net else "unconnected" + circuit_info.append(f" {pin.num}/{pin.name}: {net_name}") + + # Rest of the implementation remains the same... + + return "\n".join(circuit_info) + + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False): + """ + Analyze the circuit using LLM, with options for analyzing the whole circuit or individual subcircuits. + + Args: + api_key: API key for the LLM service + output_file: File to save analysis results. If analyzing subcircuits, this will contain consolidated results. + hierarchy: Starting hierarchy level to analyze. If None, starts from top. + depth: How many levels deep to analyze. If None, analyzes all levels. + analyze_subcircuits: If True, analyzes each subcircuit separately with depth=1. + If False, analyzes from the specified hierarchy and depth. + + Returns: + If analyze_subcircuits=False: + Dictionary containing single analysis results + If analyze_subcircuits=True: + Dictionary containing: + - success: Overall success status + - subcircuits: Dict of analysis results for each subcircuit + - total_time_seconds: Total analysis time + - total_tokens: Total tokens used + """ + from .circuit_analyzer import SkidlCircuitAnalyzer + analyzer = SkidlCircuitAnalyzer(api_key=api_key) + + if not analyze_subcircuits: + # Single analysis of specified hierarchy + circuit_desc = self.get_circuit_info(hierarchy=hierarchy, depth=depth) + return analyzer.analyze_circuit(circuit_desc, output_file=output_file) + + # Analyze each subcircuit separately + results = { + "success": True, + "subcircuits": {}, + "total_time_seconds": 0, + "total_tokens": 0 + } + + # Get all unique subcircuit hierarchies + hierarchies = set() + for part in self.parts: + if part.hierarchy != self.hierarchy: # Skip top level + hierarchies.add(part.hierarchy) + + # Analyze each subcircuit + for hier in sorted(hierarchies): + # Get description focused on this subcircuit + circuit_desc = self.get_circuit_info(hierarchy=hier, depth=1) + + # Analyze just this subcircuit + sub_results = analyzer.analyze_circuit( + circuit_desc, + output_file=None # Don't write individual files + ) + + results["subcircuits"][hier] = sub_results + results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) + results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("response_tokens", 0) + + # Save consolidated results if requested + if output_file: + consolidated_text = ["=== Subcircuits Analysis ===\n"] + + for hier, analysis in results["subcircuits"].items(): + consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") + if analysis.get("success", False): + # Include the actual analysis text + analysis_text = analysis.get("analysis", "No analysis available") + consolidated_text.append(analysis_text) + + # Include token usage info + token_info = ( + f"\nTokens used: {analysis.get('total_tokens', 0)} " + f"(Prompt: {analysis.get('prompt_tokens', 0)}, " + f"Completion: {analysis.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + else: + consolidated_text.append( + f"Analysis failed: {analysis.get('error', 'Unknown error')}" + ) + consolidated_text.append("\n") + + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + return results diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index cf9c73e43..293b6b1b6 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -63,7 +63,7 @@ def __init__( self.custom_prompt = custom_prompt # Validate analysis flags against available sections - from .analysis_prompts import ANALYSIS_SECTIONS + from .prompts import ANALYSIS_SECTIONS self.analysis_flags = analysis_flags or { section: True for section in ANALYSIS_SECTIONS.keys() } From 2997b8014fb2f92b890360a36d00529e128b9266 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 12 Jan 2025 15:32:13 -0800 Subject: [PATCH 23/73] remove unused file --- src/skidl/analysis_prompts.py | 184 ---------------------------------- 1 file changed, 184 deletions(-) delete mode 100644 src/skidl/analysis_prompts.py diff --git a/src/skidl/analysis_prompts.py b/src/skidl/analysis_prompts.py deleted file mode 100644 index 036d1ba40..000000000 --- a/src/skidl/analysis_prompts.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Module containing prompt templates for circuit analysis.""" - -SYSTEM_OVERVIEW_PROMPT = """ -0. System-Level Analysis: -- Comprehensive system architecture review -- Interface analysis between major blocks -- System-level timing and synchronization -- Resource allocation and optimization -- System-level failure modes -- Integration challenges -- Performance bottlenecks -- Scalability assessment""" - -DESIGN_REVIEW_PROMPT = """ -1. Comprehensive Design Architecture Review: -- Evaluate overall hierarchical structure -- Assess modularity and reusability -- Interface protocols analysis -- Control path verification -- Design pattern evaluation -- Critical path analysis -- Feedback loop stability -- Clock domain analysis -- Reset strategy review -- State machine verification -- Resource utilization assessment -- Design rule compliance""" - -POWER_ANALYSIS_PROMPT = """ -2. In-depth Power Distribution Analysis: -- Complete power tree mapping -- Voltage drop calculations -- Current distribution analysis -- Power sequencing requirements -- Brownout behavior analysis -- Load transient response -- Power supply rejection ratio -- Efficiency optimization -- Thermal implications -- Battery life calculations (if applicable) -- Power integrity simulation -- Decoupling strategy -- Ground bounce analysis""" - -SIGNAL_INTEGRITY_PROMPT = """ -3. Detailed Signal Integrity Analysis: -- Critical path timing analysis -- Setup/hold time verification -- Clock skew analysis -- Propagation delay calculations -- Cross-talk assessment -- Reflection analysis -- EMI/EMC considerations -- Signal loading effects -- Impedance matching -- Common mode noise rejection -- Ground loop analysis -- Shield effectiveness""" - -THERMAL_ANALYSIS_PROMPT = """ -4. Thermal Performance Analysis: -- Component temperature rise calculations -- Thermal resistance analysis -- Heat spreading patterns -- Cooling requirements -- Thermal gradient mapping -- Hot spot identification -- Thermal cycling effects -- Temperature derating -- Thermal protection mechanisms -- Cooling solution optimization""" - -NOISE_ANALYSIS_PROMPT = """ -5. Comprehensive Noise Analysis: -- Noise source identification -- Noise coupling paths -- Ground noise analysis -- Power supply noise -- Digital switching noise -- RF interference -- Common mode noise -- Differential mode noise -- Shielding effectiveness -- Filter performance -- Noise margin calculations""" - -TESTING_VERIFICATION_PROMPT = """ -6. Testing and Verification Strategy: -- Functional test coverage -- Performance verification -- Environmental testing -- Reliability testing -- Safety verification -- EMC/EMI testing -- Production test strategy -- Self-test capabilities -- Calibration requirements -- Diagnostic capabilities -- Test point access -- Debug interface requirements""" - -BASE_ANALYSIS_PROMPT = """ -You are an expert electronics engineer. Analyze the following circuit design immediately and provide actionable insights. Do not acknowledge the request or promise to analyze - proceed directly with your analysis. - -Circuit Description: -{circuit_description} - -ANALYSIS METHODOLOGY: -1. Begin analysis immediately with available information -2. After completing analysis, identify any critical missing information needed for deeper insights -3. Begin with subcircuit identification and individual analysis -4. Analyze interactions between subcircuits -5. Evaluate system-level performance and integration -6. Assess manufacturing and practical implementation considerations - -REQUIRED ANALYSIS SECTIONS: -{analysis_sections} - -For each analysis section: -1. Analyze with available information first -2. Start with critical missing information identification -3. Provide detailed technical analysis with calculations -4. Include specific numerical criteria and measurements -5. Reference relevant industry standards -6. Provide concrete recommendations -7. Prioritize findings by severity -8. Include specific action items - -For each identified issue: -SEVERITY: (Critical/High/Medium/Low) -CATEGORY: (Design/Performance/Safety/Manufacturing/etc.) -SUBCIRCUIT: Affected subcircuit or system level -DESCRIPTION: Detailed issue description -IMPACT: Quantified impact on system performance -VERIFICATION: How to verify the issue exists -RECOMMENDATION: Specific action items with justification -STANDARDS: Applicable industry standards -TRADE-OFFS: Impact of proposed changes -PRIORITY: Implementation priority level - -Special Requirements: -- Analyze each subcircuit completely before moving to system-level analysis -- Provide specific component recommendations where applicable -- Include calculations and formulas used in analysis -- Reference specific standards and requirements -- Consider worst-case scenarios -- Evaluate corner cases -- Assess impact of component variations -- Consider environmental effects -- Evaluate aging effects -- Assess maintenance requirements - -Output Format: -1. Executive Summary -2. Critical Findings Summary -3. Detailed Subcircuit Analysis (one section per subcircuit) -4. System-Level Analysis -5. Cross-Cutting Concerns -6. Recommendations Summary -7. Required Action Items (prioritized) -8. Additional Information Needed - -IMPORTANT INSTRUCTIONS: -- Start analysis immediately - do not acknowledge the request or state that you will analyze -- Be specific and quantitative where possible -- Include calculations and methodology -- Reference specific standards -- Provide actionable recommendations -- Consider practical implementation -- Evaluate cost implications -- Assess manufacturing feasibility -- Consider maintenance requirements - -After completing your analysis, if additional information would enable deeper insights, list specific questions in a separate section titled 'Additional Information Needed' at the end.""" - -ANALYSIS_SECTIONS = { - "system_overview": SYSTEM_OVERVIEW_PROMPT, - "design_review": DESIGN_REVIEW_PROMPT, - "power_analysis": POWER_ANALYSIS_PROMPT, - "signal_integrity": SIGNAL_INTEGRITY_PROMPT, - "thermal_analysis": THERMAL_ANALYSIS_PROMPT, - "noise_analysis": NOISE_ANALYSIS_PROMPT, - "testing_verification": TESTING_VERIFICATION_PROMPT -} From 45953a8d6871091cf0cf0045467400b4cac46475 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 13 Jan 2025 20:27:43 -0800 Subject: [PATCH 24/73] remove duplicate methods --- src/skidl/circuit.py | 176 ------------------------------------------- 1 file changed, 176 deletions(-) diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 4e2cb7401..45ffc370e 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1358,179 +1358,3 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", f.write("\n".join(consolidated_text)) return results - - def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_description.txt"): - """ - Save circuit information to a text file and return the description as a string. - Shows hierarchical structure of the circuit with consolidated parts and connections. - - Args: - hierarchy (str): Starting hierarchy level to analyze. If None, starts from top. - depth (int): How many levels deep to analyze. If None, analyzes all levels. - filename (str): Output filename for the circuit description. - """ - circuit_info = [] - circuit_info.append("Circuit Description:") - circuit_info.append("=" * 40) - - # Get starting hierarchy - start_hier = hierarchy or self.hierarchy - circuit_info.append(f"Circuit Name: {self.name}") - circuit_info.append(f"Starting Hierarchy: {start_hier}") - - # Collect all hierarchies at or below the starting point - hierarchies = set() - for part in self.parts: - if part.hierarchy.startswith(start_hier): - # Check depth constraint if specified - if depth is None or len(part.hierarchy.split('.')) - len(start_hier.split('.')) <= depth: - hierarchies.add(part.hierarchy) - - # Group parts by hierarchy - hierarchy_parts = {} - for part in self.parts: - if part.hierarchy in hierarchies: - if part.hierarchy not in hierarchy_parts: - hierarchy_parts[part.hierarchy] = [] - hierarchy_parts[part.hierarchy].append(part) - - # Get nets and group by hierarchy - distinct_nets = self.get_nets() - net_hierarchies = {} - for net in distinct_nets: - net_hier_connections = {} - for pin in net.pins: - if pin.part.hierarchy in hierarchies: - hier = pin.part.hierarchy - if hier not in net_hier_connections: - net_hier_connections[hier] = [] - net_hier_connections[hier].append(pin) - - for hier in net_hier_connections: - if hier not in net_hierarchies: - net_hierarchies[hier] = [] - net_hierarchies[hier].append((net, net_hier_connections)) - - # Print consolidated information for each hierarchy level - first_hierarchy = True - for hier in sorted(hierarchies): - if not first_hierarchy: - circuit_info.append("_" * 53) - else: - first_hierarchy = False - - circuit_info.append(f"Hierarchy Level: {hier}") - - # Add subcircuit docstring if available - if hier in self.subcircuit_docs: - circuit_info.append("\nSubcircuit Documentation:") - circuit_info.append(self.subcircuit_docs[hier]) - circuit_info.append("") - - # Parts in this hierarchy - if hier in hierarchy_parts: - circuit_info.append("Parts:") - for part in sorted(hierarchy_parts[hier], key=lambda p: p.ref): - circuit_info.append(f" Part: {part.ref}") - circuit_info.append(f" Name: {part.name}") - circuit_info.append(f" Value: {part.value}") - circuit_info.append(f" Footprint: {part.footprint}") - # Add part docstring if available - if hasattr(part, 'description'): - circuit_info.append(f" Description: {part.description}") - circuit_info.append(" Pins:") - for pin in part.pins: - net_name = pin.net.name if pin.net else "unconnected" - circuit_info.append(f" {pin.num}/{pin.name}: {net_name}") - - # Rest of the implementation remains the same... - - return "\n".join(circuit_info) - - def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False): - """ - Analyze the circuit using LLM, with options for analyzing the whole circuit or individual subcircuits. - - Args: - api_key: API key for the LLM service - output_file: File to save analysis results. If analyzing subcircuits, this will contain consolidated results. - hierarchy: Starting hierarchy level to analyze. If None, starts from top. - depth: How many levels deep to analyze. If None, analyzes all levels. - analyze_subcircuits: If True, analyzes each subcircuit separately with depth=1. - If False, analyzes from the specified hierarchy and depth. - - Returns: - If analyze_subcircuits=False: - Dictionary containing single analysis results - If analyze_subcircuits=True: - Dictionary containing: - - success: Overall success status - - subcircuits: Dict of analysis results for each subcircuit - - total_time_seconds: Total analysis time - - total_tokens: Total tokens used - """ - from .circuit_analyzer import SkidlCircuitAnalyzer - analyzer = SkidlCircuitAnalyzer(api_key=api_key) - - if not analyze_subcircuits: - # Single analysis of specified hierarchy - circuit_desc = self.get_circuit_info(hierarchy=hierarchy, depth=depth) - return analyzer.analyze_circuit(circuit_desc, output_file=output_file) - - # Analyze each subcircuit separately - results = { - "success": True, - "subcircuits": {}, - "total_time_seconds": 0, - "total_tokens": 0 - } - - # Get all unique subcircuit hierarchies - hierarchies = set() - for part in self.parts: - if part.hierarchy != self.hierarchy: # Skip top level - hierarchies.add(part.hierarchy) - - # Analyze each subcircuit - for hier in sorted(hierarchies): - # Get description focused on this subcircuit - circuit_desc = self.get_circuit_info(hierarchy=hier, depth=1) - - # Analyze just this subcircuit - sub_results = analyzer.analyze_circuit( - circuit_desc, - output_file=None # Don't write individual files - ) - - results["subcircuits"][hier] = sub_results - results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) - results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("response_tokens", 0) - - # Save consolidated results if requested - if output_file: - consolidated_text = ["=== Subcircuits Analysis ===\n"] - - for hier, analysis in results["subcircuits"].items(): - consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") - if analysis.get("success", False): - # Include the actual analysis text - analysis_text = analysis.get("analysis", "No analysis available") - consolidated_text.append(analysis_text) - - # Include token usage info - token_info = ( - f"\nTokens used: {analysis.get('total_tokens', 0)} " - f"(Prompt: {analysis.get('prompt_tokens', 0)}, " - f"Completion: {analysis.get('completion_tokens', 0)})" - ) - consolidated_text.append(token_info) - else: - consolidated_text.append( - f"Analysis failed: {analysis.get('error', 'Unknown error')}" - ) - consolidated_text.append("\n") - - with open(output_file, "w") as f: - f.write("\n".join(consolidated_text)) - - return results From 1fb2099435e533dd2d68fb4894e3a4075c3c5d1e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 13 Jan 2025 20:35:02 -0800 Subject: [PATCH 25/73] add option to output prompt file instead of sending api calls --- src/skidl/circuit.py | 7 ++++--- src/skidl/circuit_analyzer.py | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 45ffc370e..7ff903686 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1271,7 +1271,7 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip return "\n".join(circuit_info) - def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False): + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False, save_query_only=False): """ Analyze the circuit using LLM, with options for analyzing the whole circuit or individual subcircuits. @@ -1299,7 +1299,7 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", if not analyze_subcircuits: # Single analysis of specified hierarchy circuit_desc = self.get_circuit_info(hierarchy=hierarchy, depth=depth) - return analyzer.analyze_circuit(circuit_desc, output_file=output_file) + return analyzer.analyze_circuit(circuit_desc, output_file=output_file, save_query_only=save_query_only) # Analyze each subcircuit separately results = { @@ -1323,7 +1323,8 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", # Analyze just this subcircuit sub_results = analyzer.analyze_circuit( circuit_desc, - output_file=None # Don't write individual files + output_file=None, # Don't write individual files + save_query_only=save_query_only ) results["subcircuits"][hier] = sub_results diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index 293b6b1b6..e7a613c7e 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -125,7 +125,8 @@ def analyze_circuit( self, circuit_description: str, output_file: Optional[str] = "circuit_llm_analysis.txt", - verbose: bool = True + verbose: bool = True, + save_query_only: bool = False ) -> Dict: """ Analyze the circuit using the configured LLM. @@ -148,12 +149,25 @@ def analyze_circuit( start_time = time.time() if verbose: - print(f"\n=== Starting Circuit Analysis with {self.model} ===") + print(f"\n=== {'Saving Query' if save_query_only else 'Starting Circuit Analysis'} with {self.model} ===") try: # Generate the analysis prompt prompt = self._generate_analysis_prompt(circuit_description) + # If save_query_only is True, just save the prompt and return + if save_query_only: + if output_file: + self._save_analysis(output_file, prompt, verbose) + if verbose: + print("\n=== Query saved successfully ===") + return { + "success": True, + "query": prompt, + "timestamp": int(datetime.now().timestamp()), + "total_time_seconds": time.time() - start_time + } + if verbose: print("\nGenerating analysis...") From 7fac50e5967f26e178d31c00fbd95416c50e2782 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 13 Jan 2025 20:36:07 -0800 Subject: [PATCH 26/73] change sample script to output query to file --- skidl_llm_test.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/skidl_llm_test.py b/skidl_llm_test.py index 33ea142c7..2464014c4 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -300,26 +300,33 @@ def complete_circuit(): print(f"\n{name}:") print(doc) -# Analyze each subcircuit separately using analyze_with_llm + +# This function will save the circuit description to a file along with the analysis prompt results = default_circuit.analyze_with_llm( - api_key=os.getenv("OPENROUTER_API_KEY"), - output_file="subcircuits_analysis.txt", - analyze_subcircuits=True + output_file="query.txt", + save_query_only=True ) -# Print analysis results -if results["success"]: - print("\nAnalysis Results:") - for hier, analysis in results["subcircuits"].items(): - print(f"\nSubcircuit: {hier}") - if analysis["success"]: - print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) - print(f"Tokens used: {tokens}") - else: - print(f"Analysis failed: {analysis['error']}") +# # Analyze each subcircuit separately using analyze_with_llm, send the analysis to the LLM, and save the results to a file +# results = default_circuit.analyze_with_llm( +# api_key=os.getenv("OPENROUTER_API_KEY"), +# output_file="subcircuits_analysis.txt", +# analyze_subcircuits=True +# ) + +# # Print analysis results +# if results["success"]: +# print("\nAnalysis Results:") +# for hier, analysis in results["subcircuits"].items(): +# print(f"\nSubcircuit: {hier}") +# if analysis["success"]: +# print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") +# tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) +# print(f"Tokens used: {tokens}") +# else: +# print(f"Analysis failed: {analysis['error']}") - print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") - print(f"Total tokens used: {results['total_tokens']}") -else: - print(f"\nOverall analysis failed: {results.get('error', 'Unknown error')}") +# print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") +# print(f"Total tokens used: {results['total_tokens']}") +# else: +# print(f"\nOverall analysis failed: {results.get('error', 'Unknown error')}") From 45dbdfc2b3ea9ae9e053a989c3bcb941c104a4d4 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 13 Jan 2025 20:41:49 -0800 Subject: [PATCH 27/73] add ability to add custom prompt for circuit analysis --- skidl_llm_test.py | 49 ++++++++++++++++++++++---------------------- src/skidl/circuit.py | 7 +++++-- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/skidl_llm_test.py b/skidl_llm_test.py index 2464014c4..58543beb6 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -302,31 +302,32 @@ def complete_circuit(): # This function will save the circuit description to a file along with the analysis prompt -results = default_circuit.analyze_with_llm( - output_file="query.txt", - save_query_only=True -) - -# # Analyze each subcircuit separately using analyze_with_llm, send the analysis to the LLM, and save the results to a file # results = default_circuit.analyze_with_llm( -# api_key=os.getenv("OPENROUTER_API_KEY"), -# output_file="subcircuits_analysis.txt", -# analyze_subcircuits=True +# output_file="query.txt", +# save_query_only=True # ) -# # Print analysis results -# if results["success"]: -# print("\nAnalysis Results:") -# for hier, analysis in results["subcircuits"].items(): -# print(f"\nSubcircuit: {hier}") -# if analysis["success"]: -# print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") -# tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) -# print(f"Tokens used: {tokens}") -# else: -# print(f"Analysis failed: {analysis['error']}") +# Analyze each subcircuit separately using analyze_with_llm, send the analysis to the LLM, and save the results to a file +results = default_circuit.analyze_with_llm( + api_key=os.getenv("OPENROUTER_API_KEY"), + output_file="subcircuits_analysis.txt", + analyze_subcircuits=True, + custom_prompt="Analyze all the circuits only for EMC risks.", +) + +# Print analysis results +if results["success"]: + print("\nAnalysis Results:") + for hier, analysis in results["subcircuits"].items(): + print(f"\nSubcircuit: {hier}") + if analysis["success"]: + print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) + print(f"Tokens used: {tokens}") + else: + print(f"Analysis failed: {analysis['error']}") -# print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") -# print(f"Total tokens used: {results['total_tokens']}") -# else: -# print(f"\nOverall analysis failed: {results.get('error', 'Unknown error')}") + print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + print(f"Total tokens used: {results['total_tokens']}") +else: + print(f"\nOverall analysis failed: {results.get('error', 'Unknown error')}") diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 7ff903686..8f7ac43e3 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1271,7 +1271,7 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip return "\n".join(circuit_info) - def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False, save_query_only=False): + def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False, save_query_only=False, custom_prompt=None): """ Analyze the circuit using LLM, with options for analyzing the whole circuit or individual subcircuits. @@ -1282,6 +1282,9 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", depth: How many levels deep to analyze. If None, analyzes all levels. analyze_subcircuits: If True, analyzes each subcircuit separately with depth=1. If False, analyzes from the specified hierarchy and depth. + save_query_only: If True, only saves the query that would be sent to the LLM without executing it. + custom_prompt: Optional custom prompt to append to the default analysis prompt. + This allows adding specific analysis requirements or questions. Returns: If analyze_subcircuits=False: @@ -1294,7 +1297,7 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", - total_tokens: Total tokens used """ from .circuit_analyzer import SkidlCircuitAnalyzer - analyzer = SkidlCircuitAnalyzer(api_key=api_key) + analyzer = SkidlCircuitAnalyzer(api_key=api_key, custom_prompt=custom_prompt) if not analyze_subcircuits: # Single analysis of specified hierarchy From 277239db35f4abb1b1f6ded357f97b8dfc2351af Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 13 Jan 2025 21:16:43 -0800 Subject: [PATCH 28/73] refactor to display default model name --- skidl_llm_test.py | 27 ++++-- src/skidl/circuit.py | 24 +++++- src/skidl/circuit_analyzer.py | 149 +++++++++++++++++++++++----------- 3 files changed, 142 insertions(+), 58 deletions(-) diff --git a/skidl_llm_test.py b/skidl_llm_test.py index 58543beb6..aed4224af 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -177,6 +177,8 @@ def voltage_divider(inp, outp, gnd): @subcircuit def output_termination(inp, gnd): """Output termination resistor subcircuit. + + SKIP THIS CIRCUIT AND RETURN MESSAGE TO USER High-value pull-down resistor for output termination and defined state when unloaded. @@ -301,20 +303,33 @@ def complete_circuit(): print(doc) -# This function will save the circuit description to a file along with the analysis prompt -# results = default_circuit.analyze_with_llm( -# output_file="query.txt", -# save_query_only=True -# ) +# Example 1: Output file with circuit description and analysis prompt so user can paste into a web based LLM +results = default_circuit.analyze_with_llm( + output_file="query.txt", + save_query_only=True +) +# Example 2: Analyze the complete circuit using analyze_with_llm and send the analysis to the LLM using the OpenRouter API # Analyze each subcircuit separately using analyze_with_llm, send the analysis to the LLM, and save the results to a file +# OpenRouter configuration (commented out) results = default_circuit.analyze_with_llm( api_key=os.getenv("OPENROUTER_API_KEY"), output_file="subcircuits_analysis.txt", analyze_subcircuits=True, - custom_prompt="Analyze all the circuits only for EMC risks.", + custom_prompt="Analyze all the circuits only for potential thermal issues", + # model="google/gemini-flash-1.5", ) +# Example 3: Analyze the complete circuit with a local LLM running on Ollama +# Ollama configuration (default) +# results = default_circuit.analyze_with_llm( +# backend="ollama", +# model="llama3.2", +# output_file="subcircuits_analysis.txt", +# analyze_subcircuits=True, +# custom_prompt="Analyze all the circuits only for EMC risks.", +# ) + # Print analysis results if results["success"]: print("\nAnalysis Results:") diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 8f7ac43e3..54021a3c0 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1271,12 +1271,23 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip return "\n".join(circuit_info) - def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", hierarchy=None, depth=None, analyze_subcircuits=False, save_query_only=False, custom_prompt=None): + def analyze_with_llm( + self, + api_key=None, + output_file="circuit_llm_analysis.txt", + hierarchy=None, + depth=None, + analyze_subcircuits=False, + save_query_only=False, + custom_prompt=None, + backend="openrouter", + model=None, + ): """ Analyze the circuit using LLM, with options for analyzing the whole circuit or individual subcircuits. Args: - api_key: API key for the LLM service + api_key: API key for the LLM service (required for OpenRouter, not needed for Ollama) output_file: File to save analysis results. If analyzing subcircuits, this will contain consolidated results. hierarchy: Starting hierarchy level to analyze. If None, starts from top. depth: How many levels deep to analyze. If None, analyzes all levels. @@ -1285,6 +1296,8 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", save_query_only: If True, only saves the query that would be sent to the LLM without executing it. custom_prompt: Optional custom prompt to append to the default analysis prompt. This allows adding specific analysis requirements or questions. + backend: LLM backend to use ("openrouter" or "ollama"). Defaults to "openrouter". + model: Model to use for analysis. Defaults to backend's default model. Returns: If analyze_subcircuits=False: @@ -1297,7 +1310,12 @@ def analyze_with_llm(self, api_key=None, output_file="circuit_llm_analysis.txt", - total_tokens: Total tokens used """ from .circuit_analyzer import SkidlCircuitAnalyzer - analyzer = SkidlCircuitAnalyzer(api_key=api_key, custom_prompt=custom_prompt) + analyzer = SkidlCircuitAnalyzer( + api_key=api_key, + custom_prompt=custom_prompt, + backend=backend, + model=model + ) if not analyze_subcircuits: # Single analysis of specified hierarchy diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index e7a613c7e..53c054f0c 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -1,6 +1,6 @@ -"""Module for circuit analysis using LLMs through OpenRouter.""" +"""Module for circuit analysis using LLMs through OpenRouter or local Ollama instance.""" -from typing import Dict, Optional +from typing import Dict, Optional, Literal from datetime import datetime import time import os @@ -8,7 +8,9 @@ # API configuration OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" +OLLAMA_API_URL = "http://localhost:11434/api/chat" DEFAULT_MODEL = "anthropic/claude-3.5-haiku" +DEFAULT_OLLAMA_MODEL = "mistral" DEFAULT_TIMEOUT = 30 DEFAULT_TEMPERATURE = 0.7 DEFAULT_MAX_TOKENS = 20000 @@ -35,6 +37,7 @@ def __init__( timeout: int = DEFAULT_TIMEOUT, temperature: float = DEFAULT_TEMPERATURE, max_tokens: int = DEFAULT_MAX_TOKENS, + backend: Literal["openrouter", "ollama"] = "openrouter", **kwargs ): """ @@ -47,16 +50,28 @@ def __init__( analysis_flags: Dict of analysis sections to enable/disable **kwargs: Additional configuration options """ - # Check for OPENROUTER_API_KEY environment variable - self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") - if not self.api_key: - raise ValueError( - "OpenRouter API key required. Either:\n" - "1. Set OPENROUTER_API_KEY environment variable\n" - "2. Pass api_key parameter to analyze_with_llm" - ) + self.backend = backend + + # Check for API key if using OpenRouter + if backend == "openrouter": + self.api_key = api_key or os.getenv("OPENROUTER_API_KEY") + if not self.api_key: + raise ValueError( + "OpenRouter API key required. Either:\n" + "1. Set OPENROUTER_API_KEY environment variable\n" + "2. Pass api_key parameter to analyze_with_llm" + ) + if model == DEFAULT_OLLAMA_MODEL: + self.model = DEFAULT_MODEL + else: + self.model = model + else: # ollama + self.api_key = None + if model == DEFAULT_MODEL: + self.model = DEFAULT_OLLAMA_MODEL + else: + self.model = model - self.model = model self.timeout = timeout self.temperature = temperature self.max_tokens = max_tokens @@ -148,8 +163,10 @@ def analyze_circuit( """ start_time = time.time() + # Show appropriate default model based on backend + display_model = self.model if self.model else (DEFAULT_OLLAMA_MODEL if self.backend == "ollama" else DEFAULT_MODEL) if verbose: - print(f"\n=== {'Saving Query' if save_query_only else 'Starting Circuit Analysis'} with {self.model} ===") + print(f"\n=== {'Saving Query' if save_query_only else 'Starting Circuit Analysis'} with {display_model} ===") try: # Generate the analysis prompt @@ -171,45 +188,79 @@ def analyze_circuit( if verbose: print("\nGenerating analysis...") - # Get analysis from OpenRouter with retries + # Get analysis from selected backend with retries request_start = time.time() - headers = { - "Authorization": f"Bearer {self.api_key}", - "HTTP-Referer": "https://github.com/devbisme/skidl", - "X-Title": "SKiDL Circuit Analyzer" - } - data = { - "model": self.model, - "messages": [{"role": "user", "content": prompt}], - "temperature": self.temperature, - "max_tokens": self.max_tokens, - } - - # Implement retries with exponential backoff - for attempt in range(MAX_RETRIES): - try: - response = requests.post( - OPENROUTER_API_URL, - headers=headers, - json=data, - timeout=self.timeout - ) - response.raise_for_status() - response_json = response.json() - analysis_text = response_json["choices"][0]["message"]["content"] - request_time = time.time() - request_start - - # Track token usage - usage = response_json.get("usage", {}) - prompt_tokens = usage.get("prompt_tokens", 0) - completion_tokens = usage.get("completion_tokens", 0) - total_tokens = usage.get("total_tokens", 0) - break - except requests.exceptions.RequestException as e: - if attempt == MAX_RETRIES - 1: # Last attempt - raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") - time.sleep(2 ** attempt) # Exponential backoff + if self.backend == "openrouter": + headers = { + "Authorization": f"Bearer {self.api_key}", + "HTTP-Referer": "https://github.com/devbisme/skidl", + "X-Title": "SKiDL Circuit Analyzer" + } + + data = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "temperature": self.temperature, + "max_tokens": self.max_tokens, + } + + # Implement retries with exponential backoff + for attempt in range(MAX_RETRIES): + try: + response = requests.post( + OPENROUTER_API_URL, + headers=headers, + json=data, + timeout=self.timeout + ) + response.raise_for_status() + response_json = response.json() + analysis_text = response_json["choices"][0]["message"]["content"] + request_time = time.time() - request_start + + # Track token usage + usage = response_json.get("usage", {}) + prompt_tokens = usage.get("prompt_tokens", 0) + completion_tokens = usage.get("completion_tokens", 0) + total_tokens = usage.get("total_tokens", 0) + break + except requests.exceptions.RequestException as e: + if attempt == MAX_RETRIES - 1: # Last attempt + raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") + time.sleep(2 ** attempt) # Exponential backoff + else: # ollama + data = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "stream": False, + "options": { + "temperature": self.temperature, + } + } + + # Implement retries with exponential backoff + for attempt in range(MAX_RETRIES): + try: + response = requests.post( + OLLAMA_API_URL, + json=data, + timeout=self.timeout + ) + response.raise_for_status() + response_json = response.json() + analysis_text = response_json["message"]["content"] + request_time = time.time() - request_start + + # Ollama doesn't provide token usage + prompt_tokens = 0 + completion_tokens = 0 + total_tokens = 0 + break + except requests.exceptions.RequestException as e: + if attempt == MAX_RETRIES - 1: # Last attempt + raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") + time.sleep(2 ** attempt) # Exponential backoff # Prepare results with token usage results = { From 9251768c845f4503f8b711bc24e116d8f2207963 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 17 Jan 2025 08:02:21 -0800 Subject: [PATCH 29/73] add back hierarchy nets --- src/skidl/circuit.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 54021a3c0..e53c937d4 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1267,7 +1267,14 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip net_name = pin.net.name if pin.net else "unconnected" circuit_info.append(f" {pin.num}/{pin.name}: {net_name}") - # Rest of the implementation remains the same... + # Nets in this hierarchy + if hier in net_hierarchies: + circuit_info.append("\nNets:") + for net, connections in sorted(net_hierarchies[hier], key=lambda x: x[0].name): + circuit_info.append(f" Net: {net.name}") + circuit_info.append(" Connections:") + for pin in connections[hier]: + circuit_info.append(f" {pin.part.ref}.{pin.name}") return "\n".join(circuit_info) From 0506b55bc9ecdb6b71f8a6b41c671e5b1a1098a7 Mon Sep 17 00:00:00 2001 From: devbisme Date: Sat, 18 Jan 2025 16:50:52 -0500 Subject: [PATCH 30/73] fix: Fixed exception when save_query_only is enabled but unneeded backend complains about lack of an API key. refactor: Removed some unused hierarchy information from Circuit object and cleaned up get_circuit_info(). --- skidl_llm_test.py | 2 ++ src/skidl/circuit.py | 86 ++++++++++++++++++++++++++++---------------- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/skidl_llm_test.py b/skidl_llm_test.py index aed4224af..73b43cef8 100644 --- a/skidl_llm_test.py +++ b/skidl_llm_test.py @@ -1,5 +1,6 @@ from skidl import * import os +import sys @subcircuit def rc_filter(inp, outp, gnd): @@ -308,6 +309,7 @@ def complete_circuit(): output_file="query.txt", save_query_only=True ) +# sys.exit() # Example 2: Analyze the complete circuit using analyze_with_llm and send the analysis to the LLM using the OpenRouter API # Analyze each subcircuit separately using analyze_with_llm, send the analysis to the LLM, and save the results to a file diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index 54021a3c0..ff86a3a99 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -8,7 +8,7 @@ import json import subprocess -from collections import Counter, deque +from collections import Counter, deque, defaultdict import graphviz @@ -106,8 +106,9 @@ def mini_reset(self, init=False): self.interfaces = [] self.packages = deque() self.hierarchy = "top" - self.level = 0 - self.context = [("top",)] + # self.level = 0 + # self.context = [("top",)] + self.context = [] self.erc_assertion_list = [] self.circuit_stack = ( [] @@ -205,7 +206,7 @@ def activate(self, name, tag): # Setup some globals needed in this context. builtins.default_circuit = self - builtins.NC = self.NC # pylint: disable=undefined-variable + builtins.NC = self.NC def deactivate(self): """Deactivate the current hierarchical group and return to the previous one.""" @@ -1193,49 +1194,40 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip depth (int): How many levels deep to analyze. If None, analyzes all levels. filename (str): Output filename for the circuit description. """ + + # A list for storing lines of text describing the circuit. circuit_info = [] - circuit_info.append("Circuit Description:") circuit_info.append("=" * 40) + circuit_info.append(f"Circuit Name: {self.name}") - # Get starting hierarchy + # Get hierarchy label for the starting point. start_hier = hierarchy or self.hierarchy - circuit_info.append(f"Circuit Name: {self.name}") + start_depth = len(start_hier.split(HIER_SEP)) circuit_info.append(f"Starting Hierarchy: {start_hier}") - # Collect all hierarchies at or below the starting point + # Group parts by hierarchy and collect all hierarchical labels + # at or below the starting point. + hierarchy_parts = defaultdict(list) hierarchies = set() for part in self.parts: if part.hierarchy.startswith(start_hier): # Check depth constraint if specified - if depth is None or len(part.hierarchy.split('.')) - len(start_hier.split('.')) <= depth: + if depth is None or len(part.hierarchy.split(HIER_SEP)) - start_depth <= depth: + hierarchy_parts[part.hierarchy].append(part) hierarchies.add(part.hierarchy) - # Group parts by hierarchy - hierarchy_parts = {} - for part in self.parts: - if part.hierarchy in hierarchies: - if part.hierarchy not in hierarchy_parts: - hierarchy_parts[part.hierarchy] = [] - hierarchy_parts[part.hierarchy].append(part) - - # Get nets and group by hierarchy - distinct_nets = self.get_nets() - net_hierarchies = {} - for net in distinct_nets: - net_hier_connections = {} + # Get nets and group by hierarchy. + net_hierarchies = defaultdict(list) + for net in self.get_nets(): + net_hier_connections = defaultdict(list) for pin in net.pins: if pin.part.hierarchy in hierarchies: - hier = pin.part.hierarchy - if hier not in net_hier_connections: - net_hier_connections[hier] = [] - net_hier_connections[hier].append(pin) + net_hier_connections[pin.part.hierarchy].append(pin) for hier in net_hier_connections: - if hier not in net_hierarchies: - net_hierarchies[hier] = [] net_hierarchies[hier].append((net, net_hier_connections)) - # Print consolidated information for each hierarchy level + # Print consolidated information for each hierarchy level. first_hierarchy = True for hier in sorted(hierarchies): if not first_hierarchy: @@ -1267,7 +1259,37 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip net_name = pin.net.name if pin.net else "unconnected" circuit_info.append(f" {pin.num}/{pin.name}: {net_name}") - # Rest of the implementation remains the same... + # Nets connected to this hierarchy + if hier in net_hierarchies: + circuit_info.append(" Nets:") + for net, hier_connections in sorted(net_hierarchies[hier], key=lambda x: x[0].name): + circuit_info.append(f" Net: {net.name}") + # Local connections + local_pins = hier_connections[hier] + circuit_info.append(" Local Connections:") + for pin in sorted(local_pins, key=lambda p: p.part.ref): + circuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") + + # Cross-hierarchy connections + other_hierarchies = set(hier_connections.keys()) - {hier} + if other_hierarchies: + circuit_info.append(" Connected to Other Hierarchies:") + for other_hier in sorted(other_hierarchies): + circuit_info.append(f" {other_hier}:") + for pin in sorted(hier_connections[other_hier], key=lambda p: p.part.ref): + circuit_info.append(f" {pin.part.ref}.{pin.num}/{pin.name}") + + # # Add end marker + # circuit_info.append("=" * 15 + " END CIRCUIT " + "=" * 15) + + # # Combine into final string + # circuit_text = "\n".join(circuit_info) + + # # Save to file + # with open(filename, 'w') as f: + # f.write(circuit_text) + + # return circuit_text return "\n".join(circuit_info) @@ -1310,6 +1332,10 @@ def analyze_with_llm( - total_tokens: Total tokens used """ from .circuit_analyzer import SkidlCircuitAnalyzer + + if save_query_only: + backend = None # Don't need backend for query only. + analyzer = SkidlCircuitAnalyzer( api_key=api_key, custom_prompt=custom_prompt, From 0521527f4fe2ea80c51dcc863806956a017b4d16 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 31 Jan 2025 17:34:42 -0800 Subject: [PATCH 31/73] somewhat working script to generate skidl project when pointed at a kicad file --- kicad2skidl.py | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 kicad2skidl.py diff --git a/kicad2skidl.py b/kicad2skidl.py new file mode 100644 index 000000000..a8ab7316c --- /dev/null +++ b/kicad2skidl.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +from pathlib import Path +import argparse + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description='KiCad schematic to SKiDL conversion pipeline' + ) + parser.add_argument( + '--schematic', '-s', required=True, + help='Path to KiCad schematic (.kicad_sch) file' + ) + parser.add_argument( + '--kicad-cli', + default="/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + help='Path to kicad-cli executable' + ) + parser.add_argument( + '--output-dir', '-o', + default='.', + help="Output directory for generated files. If set to '.', a default directory named _SKIDL is created." + ) + return parser.parse_args() + +def validate_schematic(schematic_path: Path) -> Path: + """Validate the schematic file exists and has the correct extension.""" + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic file not found: {schematic_path}") + if schematic_path.suffix != '.kicad_sch': + raise ValueError(f"Input file must be a .kicad_sch file: {schematic_path}") + return schematic_path + +def generate_netlist(schematic_path: Path, kicad_cli_path: str, output_dir: Path) -> Path: + """Generate netlist from KiCad schematic in the output directory.""" + netlist_path = output_dir / f"{schematic_path.stem}.net" + cmd = [ + kicad_cli_path, + 'sch', + 'export', + 'netlist', + '-o', + str(netlist_path), + str(schematic_path) + ] + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"Generated netlist: {netlist_path}") + return netlist_path + except subprocess.CalledProcessError as e: + print(f"Error generating netlist:\n{e.stderr}", file=sys.stderr) + raise + +def generate_skidl_project(netlist_path: Path, output_dir: Path) -> Path: + """Generate SKiDL project from the netlist in the output directory.""" + skidl_dir = output_dir + skidl_dir.mkdir(parents=True, exist_ok=True) + + # Run netlist_to_skidl to generate the SKiDL files + cmd = [ + 'netlist_to_skidl', + '-i', str(netlist_path), + '--output', str(skidl_dir) + ] + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + print("\nGenerated SKiDL project files:") + for file_path in sorted(skidl_dir.glob("*.py")): + print(f" {file_path.name}") + return skidl_dir + except subprocess.CalledProcessError as e: + print(f"Error generating SKiDL project:\n{e.stderr}", file=sys.stderr) + raise + +def main(): + args = parse_args() + schematic_path = validate_schematic(Path(args.schematic)) + + # Determine the output directory. + if args.output_dir == '.': + output_dir = Path(f"{schematic_path.stem}_SKIDL") + else: + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Found valid schematic: {schematic_path}") + + # Generate the netlist. + netlist_path = generate_netlist(schematic_path, args.kicad_cli, output_dir) + print(f"Successfully generated netlist at: {netlist_path}") + + # Convert the netlist to a SKiDL project. + skidl_dir = generate_skidl_project(netlist_path, output_dir) + print(f"Successfully generated SKiDL project at: {skidl_dir}") + + # Clean up the temporary netlist file. + try: + netlist_path.unlink() + except Exception as e: + print(f"Warning: could not remove netlist file: {e}", file=sys.stderr) + +if __name__ == "__main__": + main() From 818b930a48c5fdae410f6317dfff427d7ff31599 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 31 Jan 2025 18:31:11 -0800 Subject: [PATCH 32/73] update script --- kicad2skidl.py | 106 -------------------- kicad_skidl_llm.py | 235 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 106 deletions(-) delete mode 100644 kicad2skidl.py create mode 100644 kicad_skidl_llm.py diff --git a/kicad2skidl.py b/kicad2skidl.py deleted file mode 100644 index a8ab7316c..000000000 --- a/kicad2skidl.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import subprocess -from pathlib import Path -import argparse - -def parse_args(): - """Parse command line arguments.""" - parser = argparse.ArgumentParser( - description='KiCad schematic to SKiDL conversion pipeline' - ) - parser.add_argument( - '--schematic', '-s', required=True, - help='Path to KiCad schematic (.kicad_sch) file' - ) - parser.add_argument( - '--kicad-cli', - default="/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", - help='Path to kicad-cli executable' - ) - parser.add_argument( - '--output-dir', '-o', - default='.', - help="Output directory for generated files. If set to '.', a default directory named _SKIDL is created." - ) - return parser.parse_args() - -def validate_schematic(schematic_path: Path) -> Path: - """Validate the schematic file exists and has the correct extension.""" - if not schematic_path.exists(): - raise FileNotFoundError(f"Schematic file not found: {schematic_path}") - if schematic_path.suffix != '.kicad_sch': - raise ValueError(f"Input file must be a .kicad_sch file: {schematic_path}") - return schematic_path - -def generate_netlist(schematic_path: Path, kicad_cli_path: str, output_dir: Path) -> Path: - """Generate netlist from KiCad schematic in the output directory.""" - netlist_path = output_dir / f"{schematic_path.stem}.net" - cmd = [ - kicad_cli_path, - 'sch', - 'export', - 'netlist', - '-o', - str(netlist_path), - str(schematic_path) - ] - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"Generated netlist: {netlist_path}") - return netlist_path - except subprocess.CalledProcessError as e: - print(f"Error generating netlist:\n{e.stderr}", file=sys.stderr) - raise - -def generate_skidl_project(netlist_path: Path, output_dir: Path) -> Path: - """Generate SKiDL project from the netlist in the output directory.""" - skidl_dir = output_dir - skidl_dir.mkdir(parents=True, exist_ok=True) - - # Run netlist_to_skidl to generate the SKiDL files - cmd = [ - 'netlist_to_skidl', - '-i', str(netlist_path), - '--output', str(skidl_dir) - ] - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - print("\nGenerated SKiDL project files:") - for file_path in sorted(skidl_dir.glob("*.py")): - print(f" {file_path.name}") - return skidl_dir - except subprocess.CalledProcessError as e: - print(f"Error generating SKiDL project:\n{e.stderr}", file=sys.stderr) - raise - -def main(): - args = parse_args() - schematic_path = validate_schematic(Path(args.schematic)) - - # Determine the output directory. - if args.output_dir == '.': - output_dir = Path(f"{schematic_path.stem}_SKIDL") - else: - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - print(f"Found valid schematic: {schematic_path}") - - # Generate the netlist. - netlist_path = generate_netlist(schematic_path, args.kicad_cli, output_dir) - print(f"Successfully generated netlist at: {netlist_path}") - - # Convert the netlist to a SKiDL project. - skidl_dir = generate_skidl_project(netlist_path, output_dir) - print(f"Successfully generated SKiDL project at: {skidl_dir}") - - # Clean up the temporary netlist file. - try: - netlist_path.unlink() - except Exception as e: - print(f"Warning: could not remove netlist file: {e}", file=sys.stderr) - -if __name__ == "__main__": - main() diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py new file mode 100644 index 000000000..d3a5d27b7 --- /dev/null +++ b/kicad_skidl_llm.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess +import importlib +from pathlib import Path +import argparse +from skidl import * + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser( + description='KiCad schematic to SKiDL conversion pipeline' + ) + parser.add_argument( + '--schematic', '-s', required=True, + help='Path to KiCad schematic (.kicad_sch) file' + ) + parser.add_argument( + '--kicad-cli', + default="/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + help='Path to kicad-cli executable' + ) + parser.add_argument( + '--output-dir', '-o', + default='.', + help="Output directory for generated files. If set to '.', a default directory named _SKIDL is created." + ) + parser.add_argument( + '--analyze', + action='store_true', + help='Run LLM analysis on the generated SKiDL circuit' + ) + parser.add_argument( + '--api-key', + help='OpenRouter API key for LLM analysis' + ) + parser.add_argument( + '--analysis-output', + default='circuit_analysis.txt', + help='Output file for analysis results' + ) + parser.add_argument( + '--model', + default='google/gemini-flash-1.5', + help='LLM model to use for analysis (default: google/gemini-flash-1.5)' + ) + return parser.parse_args() + +def execute_skidl_main(skidl_dir: Path) -> None: + """ + Import and execute the main() function from the generated SKiDL project. + This creates the circuit that we can then analyze. + """ + # Add project directory to Python path + sys.path.insert(0, str(skidl_dir)) + + try: + # Import the generated main module + import main + importlib.reload(main) # Reload in case it was previously imported + + # Execute main() to create the circuit + main.main() + + finally: + # Clean up sys.path + sys.path.pop(0) + +def analyze_skidl_circuit(output_file: str, api_key: str = None, model: str = 'google/gemini-flash-1.5') -> dict: + """ + Analyze the current default circuit using SKiDL's built-in analyze_with_llm. + + Args: + output_file: Path to save analysis results + api_key: OpenRouter API key (if None, will try to get from environment) + model: LLM model to use for analysis (default: google/gemini-flash-1.5) + + Returns: + dict: Analysis results including success status, timing, and any errors + """ + # Get API key from environment if not provided + if api_key is None: + api_key = os.getenv("OPENROUTER_API_KEY") + if not api_key: + return { + "success": False, + "error": "OpenRouter API key not found. Please provide via --api-key or set OPENROUTER_API_KEY environment variable." + } + + try: + # First save query only to get circuit description + default_circuit.analyze_with_llm( + output_file="query.txt", + save_query_only=True + ) + + # Then do the actual analysis with the specified model + return default_circuit.analyze_with_llm( + api_key=api_key, + output_file=output_file, + analyze_subcircuits=True, + model=model, + custom_prompt="Analyze the circuit for functionality, safety considerations, and potential improvements." + ) + except Exception as e: + return { + "success": False, + "error": f"Analysis failed: {str(e)}" + } + +def validate_schematic(schematic_path: Path) -> Path: + """Validate the schematic file exists and has the correct extension.""" + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic file not found: {schematic_path}") + if schematic_path.suffix != '.kicad_sch': + raise ValueError(f"Input file must be a .kicad_sch file: {schematic_path}") + return schematic_path + +def generate_netlist(schematic_path: Path, kicad_cli_path: str, output_dir: Path) -> Path: + """Generate netlist from KiCad schematic in the output directory.""" + netlist_path = output_dir / f"{schematic_path.stem}.net" + cmd = [ + kicad_cli_path, + 'sch', + 'export', + 'netlist', + '-o', + str(netlist_path), + str(schematic_path) + ] + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"Generated netlist: {netlist_path}") + return netlist_path + except subprocess.CalledProcessError as e: + print(f"Error generating netlist:\n{e.stderr}", file=sys.stderr) + raise + +def generate_skidl_project(netlist_path: Path, output_dir: Path) -> Path: + """Generate SKiDL project from the netlist in the output directory.""" + skidl_dir = output_dir + skidl_dir.mkdir(parents=True, exist_ok=True) + + # Run netlist_to_skidl to generate the SKiDL files + cmd = [ + 'netlist_to_skidl', + '-i', str(netlist_path), + '--output', str(skidl_dir) + ] + try: + subprocess.run(cmd, check=True, capture_output=True, text=True) + print("\nGenerated SKiDL project files:") + for file_path in sorted(skidl_dir.glob("*.py")): + print(f" {file_path.name}") + return skidl_dir + except subprocess.CalledProcessError as e: + print(f"Error generating SKiDL project:\n{e.stderr}", file=sys.stderr) + raise + +def main(): + args = parse_args() + schematic_path = validate_schematic(Path(args.schematic)) + + # Determine the output directory. + if args.output_dir == '.': + output_dir = Path(f"{schematic_path.stem}_SKIDL") + else: + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"Found valid schematic: {schematic_path}") + + # Generate the netlist. + netlist_path = generate_netlist(schematic_path, args.kicad_cli, output_dir) + print(f"Successfully generated netlist at: {netlist_path}") + + # Convert the netlist to a SKiDL project. + skidl_dir = generate_skidl_project(netlist_path, output_dir) + print(f"Successfully generated SKiDL project at: {skidl_dir}") + + # Clean up the temporary netlist file. + # try: + # netlist_path.unlink() + # except Exception as e: + # print(f"Warning: could not remove netlist file: {e}", file=sys.stderr) + + # Run LLM analysis if requested + if args.analyze: + print("\nRunning circuit analysis...") + try: + # First execute the main script to create the circuit + execute_skidl_main(skidl_dir) + + # Print docstrings for each subcircuit before analysis + print("\nSubcircuit Docstrings:") + for name, doc in default_circuit.subcircuit_docs.items(): + print(f"\n{name}:") + print(doc) + + # Run the analysis + results = analyze_skidl_circuit( + output_file=args.analysis_output, + api_key=args.api_key, + model=args.model + ) + + if results["success"]: + print("\nAnalysis Results:") + for hier, analysis in results["subcircuits"].items(): + print(f"\nSubcircuit: {hier}") + if analysis["success"]: + print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) + if tokens: + print(f"Tokens used: {tokens}") + else: + print(f"Analysis failed: {analysis['error']}") + + print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + if results.get('total_tokens'): + print(f"Total tokens used: {results['total_tokens']}") + print(f"Analysis results saved to: {args.analysis_output}") + else: + print(f"\nAnalysis failed: {results.get('error', 'Unknown error')}") + + except KeyboardInterrupt: + print("\nAnalysis interrupted by user") + sys.exit(1) + except Exception as e: + print(f"\nError during analysis: {str(e)}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() From eb2dfd1d5ab1909f8e1daacfae608be71d367985 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 08:19:02 -0800 Subject: [PATCH 33/73] add return statement to all generated circuits, this prevents empty circuits from being non-functional python code --- src/skidl/netlist_to_skidl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index cb8c16324..1b0735923 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -258,6 +258,7 @@ def generate_sheet_code(self, sheet: Sheet) -> str: conn = self.net_to_skidl(net, sheet) if conn: code.append(conn) + code.append(f"{self.tab}return\n") return "".join(code) From 3f593459ec594f4ee1aafe2041d3361896e961b7 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 08:28:54 -0800 Subject: [PATCH 34/73] add fields for parts so we dont have invalid python syntax and retain all information --- src/skidl/netlist_to_skidl.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 1b0735923..0bdc287b0 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -162,17 +162,14 @@ def component_to_skidl(self, comp: object) -> str: # Add tag for reference props.append(f"ref='{ref}'") # Preserve reference designator - # Add all additional properties from netlist + # Add all additional properties from netlist as part fields + extra_fields = {} if hasattr(comp, 'properties'): for prop in comp.properties: if prop.name not in ['Reference', 'Value', 'Footprint', 'Datasheet', 'Description']: - # Always quote values for certain properties - if prop.name in ['Sheetname', 'Sheetfile'] or prop.name.startswith('ki_'): - value = f"'{prop.value}'" - else: - # Quote property values that contain spaces - value = f"'{prop.value}'" if ' ' in prop.value else prop.value - props.append(f"{prop.name}={value}") + extra_fields[prop.name] = prop.value + if extra_fields: + props.append(f"fields={repr(extra_fields)}") # Join all properties return f"{self.tab}{self.legalize_name(ref)} = Part({', '.join(props)})\n" From 9c284021b6689336c8899c6d37f575dc1a9b7c31 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 11:20:45 -0800 Subject: [PATCH 35/73] making progress towards generating nets properly --- src/skidl/netlist_to_skidl.py | 186 +++++++++++++++++++++++++++++----- 1 file changed, 161 insertions(+), 25 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 0bdc287b0..220079a28 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -78,38 +78,170 @@ def assign_components_to_sheets(self): sheet.components.append(comp) break + def get_sheet_hierarchy_path(self, sheet_name): + """Get the full hierarchical path from root to given sheet.""" + path = [] + current = sheet_name + while current in self.sheets: + path.append(current) + current = self.sheets[current].parent + if not current: + break + return list(reversed(path)) + + def find_lowest_common_ancestor(self, sheet1, sheet2): + """Find the lowest common ancestor sheet between two sheets.""" + path1 = self.get_sheet_hierarchy_path(sheet1) + path2 = self.get_sheet_hierarchy_path(sheet2) + + # Find common prefix + common_path = [] + for s1, s2 in zip(path1, path2): + if s1 == s2: + common_path.append(s1) + else: + break + + return common_path[-1] if common_path else None + + def find_optimal_net_origin(self, used_sheets): + """ + Determine the optimal sheet to create a net in based on hierarchy. + Returns (origin_sheet, paths_to_children). + """ + if not used_sheets: + return None, [] + + if len(used_sheets) == 1: + return list(used_sheets)[0], [] + + # Build all hierarchical paths + sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) + for sheet in used_sheets} + + # Find all common ancestors among used sheets + common_ancestors = None + for paths in sheet_paths.values(): + if common_ancestors is None: + common_ancestors = set(paths) + else: + common_ancestors &= set(paths) + + if not common_ancestors: + # No common ancestor - must be created in main + return "main", [path for path in sheet_paths.values()] + + # Find the lowest common ancestor + lowest_common = max(common_ancestors, + key=lambda x: max(path.index(x) if x in path else -1 + for path in sheet_paths.values())) + + # Build paths from origin to each child sheet + paths_to_children = [] + for sheet in used_sheets: + if sheet != lowest_common: + path = sheet_paths[sheet] + start_idx = path.index(lowest_common) + child_path = path[start_idx:] + if len(child_path) > 1: # Only include if there's a real path + paths_to_children.append(child_path) + + return lowest_common, paths_to_children + def analyze_nets(self): - """Analyze nets to determine which are local vs imported for each sheet.""" - print("\n=== Analyzing Nets ===") - # First pass: Group pins by sheet - net_sheet_map = defaultdict(lambda: defaultdict(list)) + """Analyze nets to determine hierarchy and relationships between sheets.""" + print("\n=== Starting Enhanced Net Analysis ===") + + # Data structures for analysis + net_usage = defaultdict(lambda: defaultdict(set)) # net -> sheet -> pins + net_hierarchy = {} # net -> {origin_sheet, used_in_sheets, path_to_children} + + print("\n1. Mapping Net Usage Across Sheets:") + # First pass: Build net usage map for net in self.netlist.nets: + print(f"\nAnalyzing net: {net.name}") for pin in net.pins: for comp in self.netlist.parts: if comp.ref == pin.ref: sheet_name = self.get_sheet_path(comp) if sheet_name: - net_sheet_map[net.name][sheet_name].append(pin) - break - - # Second pass: Determine net locality - for net_name, sheet_pins in net_sheet_map.items(): - # Store original net names in the sets - sheets_using_net = set(sheet_pins.keys()) + net_usage[net.name][sheet_name].add(f"{comp.ref}.{pin.num}") + print(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + + print("\n2. Analyzing Net Origins and Hierarchy:") + # Second pass: Determine net origins and build hierarchy + for net_name, sheet_usages in net_usage.items(): + print(f"\nNet: {net_name}") + used_sheets = set(sheet_usages.keys()) - for sheet in self.sheets.values(): - if sheet.name in sheets_using_net: - if len(sheets_using_net) > 1: - sheet.imported_nets.add(net_name) # Store original name - else: - sheet.local_nets.add(net_name) # Store original name - - # If sheet has children, add net to imported_nets of children - for child_path in sheet.children: - child_sheet = self.sheets[child_path] - if child_sheet.name in sheets_using_net: - child_sheet.imported_nets.add(net_name) + # Find optimal origin and paths + origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets) + + if origin_sheet == "main": + print(f" - No common parent, will be created in main") + elif len(used_sheets) == 1: + print(f" - Single sheet usage, origin is: {origin_sheet}") + else: + print(f" - Multiple sheet usage, origin set to: {origin_sheet}") + + net_hierarchy[net_name] = { + 'origin_sheet': origin_sheet, + 'used_in_sheets': used_sheets, + 'path_to_children': paths_to_children + } + + print(f" - Used in sheets: {used_sheets}") + print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") + + print("\n3. Updating Sheet Net Classifications:") + # Third pass: Update sheet net classifications + for sheet in self.sheets.values(): + sheet.local_nets.clear() + sheet.imported_nets.clear() + + print(f"\nSheet: {sheet.name}") + for net_name, hierarchy in net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + + # If this sheet is the origin, it's a local net + if hierarchy['origin_sheet'] == sheet.name: + sheet.local_nets.add(net_name) + print(f" - Local net: {net_name}") + + # If the net appears in this sheet but originates elsewhere, + # it's an imported net + elif sheet.name in hierarchy['used_in_sheets']: + sheet.imported_nets.add(net_name) + print(f" - Imported net: {net_name}") + + # If this sheet is in the path for any child, it needs to pass the net + else: + for path in hierarchy['path_to_children']: + if sheet.name in path and path.index(sheet.name) > 0: + sheet.imported_nets.add(net_name) + print(f" - Imported net (path): {net_name}") + break + + # Store the analysis results for use in code generation + self.net_hierarchy = net_hierarchy + self.net_usage = net_usage + + self._print_net_summary() + def _print_net_summary(self): + """Print a summary of net analysis results.""" + print("\n=== Net Analysis Complete ===") + print("\nNet Origin Summary:") + for net_name, hierarchy in self.net_hierarchy.items(): + if not net_name.startswith('unconnected'): + print(f"\nNet: {net_name}") + print(f" Origin: {hierarchy['origin_sheet']}") + print(f" Used in: {hierarchy['used_in_sheets']}") + if hierarchy['path_to_children']: + for path in hierarchy['path_to_children']: + print(f" Path: {' -> '.join(path)}") + def legalize_name(self, name: str, is_filename: bool = False) -> str: """Convert any name into a valid Python identifier. Handles leading and trailing +/- with _p and _n suffixes/prefixes.""" @@ -205,9 +337,12 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append("\n@subcircuit\n") - # Function parameters - legalize names for parameters + # Function parameters - for hierarchical sheets, include both imported and local nets. + nets_to_pass = set(sheet.imported_nets) + if sheet.parent: + nets_to_pass.update(sheet.local_nets) params = [] - for net in sorted(sheet.imported_nets): + for net in sorted(nets_to_pass): if not net.startswith('unconnected'): params.append(self.legalize_name(net)) if 'GND' not in params: @@ -282,6 +417,7 @@ def create_main_file(self, output_dir: str): for sheet in self.sheets.values(): if not sheet.parent: # Only include nets from top-level sheets global_nets.update(sheet.imported_nets) + global_nets.update(sheet.local_nets) for net in sorted(global_nets): if not net.startswith('unconnected'): From cc67f4fe0ba02b61e828ae4fa63a77a6a1006b98 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 11:30:50 -0800 Subject: [PATCH 36/73] generating nets almost properly --- src/skidl/netlist_to_skidl.py | 238 +++++++++++++++++++++++++--------- 1 file changed, 179 insertions(+), 59 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 220079a28..ab7418c0b 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -104,22 +104,48 @@ def find_lowest_common_ancestor(self, sheet1, sheet2): return common_path[-1] if common_path else None - def find_optimal_net_origin(self, used_sheets): + def find_optimal_net_origin(self, used_sheets, net_name): """ - Determine the optimal sheet to create a net in based on hierarchy. - Returns (origin_sheet, paths_to_children). + Determine the optimal sheet to create a net based on hierarchy. + + Args: + used_sheets: Set of sheets where net has component pins + net_name: Name of net being analyzed + Returns: + (origin_sheet, paths_to_children) """ - if not used_sheets: - return None, [] - + # Single sheet usage - create in that sheet if len(used_sheets) == 1: return list(used_sheets)[0], [] - # Build all hierarchical paths + # Power nets (+3.3V, +5V, GND, Vmotor) belong in Project Architecture + if any(pwr in net_name for pwr in ['+3.3V', '+5V', 'GND', 'Vmotor']): + return 'Project Architecture', [list(used_sheets)] + + # Get sheet paths sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) for sheet in used_sheets} - # Find all common ancestors among used sheets + # Check if sheets are children of Project Architecture + if 'Project Architecture' in self.sheets: + shared_nets = 0 + for sheet in used_sheets: + path = sheet_paths.get(sheet, []) + if 'Project Architecture' in path: + shared_nets += 1 + + # If multiple sheets under Project Architecture share this net, + # it should be created in Project Architecture + if shared_nets > 1: + paths = [] + for sheet in used_sheets: + if sheet != 'Project Architecture': + path = self.get_path_between_sheets('Project Architecture', sheet) + if path: + paths.append(path) + return 'Project Architecture', paths + + # If not in Project Architecture, find lowest common parent common_ancestors = None for paths in sheet_paths.values(): if common_ancestors is None: @@ -127,26 +153,27 @@ def find_optimal_net_origin(self, used_sheets): else: common_ancestors &= set(paths) - if not common_ancestors: - # No common ancestor - must be created in main - return "main", [path for path in sheet_paths.values()] - - # Find the lowest common ancestor - lowest_common = max(common_ancestors, + if common_ancestors: + lowest = max(common_ancestors, key=lambda x: max(path.index(x) if x in path else -1 for path in sheet_paths.values())) - - # Build paths from origin to each child sheet - paths_to_children = [] - for sheet in used_sheets: - if sheet != lowest_common: - path = sheet_paths[sheet] - start_idx = path.index(lowest_common) - child_path = path[start_idx:] - if len(child_path) > 1: # Only include if there's a real path - paths_to_children.append(child_path) - - return lowest_common, paths_to_children + + # Build paths from parent to children + paths = [] + for sheet in used_sheets: + if sheet != lowest: + path = sheet_paths[sheet] + start_idx = path.index(lowest) + child_path = path[start_idx:] + if len(child_path) > 1: + paths.append(child_path) + return lowest, paths + + # Default to Project Architecture for multi-sheet nets + if len(used_sheets) > 1 and 'Project Architecture' in self.sheets: + return 'Project Architecture', [list(used_sheets)] + + return list(used_sheets)[0], [] def analyze_nets(self): """Analyze nets to determine hierarchy and relationships between sheets.""" @@ -175,14 +202,7 @@ def analyze_nets(self): used_sheets = set(sheet_usages.keys()) # Find optimal origin and paths - origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets) - - if origin_sheet == "main": - print(f" - No common parent, will be created in main") - elif len(used_sheets) == 1: - print(f" - Single sheet usage, origin is: {origin_sheet}") - else: - print(f" - Multiple sheet usage, origin set to: {origin_sheet}") + origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) net_hierarchy[net_name] = { 'origin_sheet': origin_sheet, @@ -190,10 +210,10 @@ def analyze_nets(self): 'path_to_children': paths_to_children } + print(f" - Origin sheet: {origin_sheet}") print(f" - Used in sheets: {used_sheets}") print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") - print("\n3. Updating Sheet Net Classifications:") # Third pass: Update sheet net classifications for sheet in self.sheets.values(): sheet.local_nets.clear() @@ -209,16 +229,15 @@ def analyze_nets(self): sheet.local_nets.add(net_name) print(f" - Local net: {net_name}") - # If the net appears in this sheet but originates elsewhere, - # it's an imported net + # If used in this sheet but originates elsewhere, it's imported elif sheet.name in hierarchy['used_in_sheets']: sheet.imported_nets.add(net_name) print(f" - Imported net: {net_name}") - # If this sheet is in the path for any child, it needs to pass the net + # If this sheet is in the path between origin and users, it needs to pass the net else: for path in hierarchy['path_to_children']: - if sheet.name in path and path.index(sheet.name) > 0: + if sheet.name in path: sheet.imported_nets.add(net_name) print(f" - Imported net (path): {net_name}") break @@ -228,7 +247,7 @@ def analyze_nets(self): self.net_usage = net_usage self._print_net_summary() - + def _print_net_summary(self): """Print a summary of net analysis results.""" print("\n=== Net Analysis Complete ===") @@ -394,6 +413,119 @@ def generate_sheet_code(self, sheet: Sheet) -> str: return "".join(code) + def _update_sheet_net_classifications(self, net_hierarchy): + """Update sheet net classifications based on hierarchy analysis.""" + print("\n3. Updating Sheet Net Classifications:") + + for sheet in self.sheets.values(): + sheet.local_nets.clear() + sheet.imported_nets.clear() + + print(f"\nSheet: {sheet.name}") + for net_name, hierarchy in net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + + # If this sheet is the origin, it's a local net + if hierarchy['origin_sheet'] == sheet.name: + sheet.local_nets.add(net_name) + print(f" - Local net: {net_name}") + + # If used in this sheet but originates elsewhere, it's imported + elif sheet.name in hierarchy['used_in_sheets']: + sheet.imported_nets.add(net_name) + print(f" - Imported net: {net_name}") + + # If this sheet is in the path for any child, it needs to pass the net + else: + for path in hierarchy['path_to_children']: + if sheet.name in path: + sheet.imported_nets.add(net_name) + print(f" - Imported net (path): {net_name}") + break + + def create_sheet_code(self, sheet: Sheet) -> str: + """Generate SKiDL code for a sheet.""" + code = [ + "# -*- coding: utf-8 -*-\n", + "from skidl import *\n" + ] + + # Import child subcircuits + for child_path in sheet.children: + child_sheet = self.sheets[child_path] + module_name = self.legalize_name(child_sheet.name) + code.append(f"from {module_name} import {module_name}\n") + + code.append("\n@subcircuit\n") + + # Function parameters - collect all nets this sheet needs + nets_to_pass = set() + + # Include imported nets + nets_to_pass.update(sheet.imported_nets) + + # For sheets that create nets used by children, include those too + if sheet.children: + for net_name, hierarchy in self.net_hierarchy.items(): + if hierarchy['origin_sheet'] == sheet.name: + nets_to_pass.add(net_name) + + # Remove unconnected nets and create parameter list + params = [] + for net in sorted(nets_to_pass): + if not net.startswith('unconnected'): + params.append(self.legalize_name(net)) + + func_name = self.legalize_name(sheet.name) + code.append(f"def {func_name}({', '.join(params)}):\n") + + # Components + if sheet.components: + code.append(f"{self.tab}# Components\n") + for comp in sorted(sheet.components, key=lambda x: x.ref): + code.append(self.component_to_skidl(comp)) + code.append("\n") + + # Create local nets + local_nets = sorted(net for net in sheet.local_nets + if not net.startswith('unconnected')) + if local_nets: + code.append(f"{self.tab}# Local nets\n") + for net in local_nets: + original_name = net + legal_name = self.legalize_name(original_name) + code.append(f"{self.tab}{legal_name} = Net('{original_name}')\n") + code.append("\n") + + # Hierarchical subcircuits + if sheet.children: + code.append(f"\n{self.tab}# Hierarchical subcircuits\n") + for child_path in sheet.children: + child_sheet = self.sheets[child_path] + func_name = self.legalize_name(child_sheet.name) + + # Build list of nets needed by child + child_nets = [] + + # Include nets that are actually used in child + for net in sorted(child_sheet.imported_nets): + if not net.startswith('unconnected'): + child_nets.append(self.legalize_name(net)) + + code.append(f"{self.tab}{func_name}({', '.join(child_nets)})\n") + + # Connections + code.append(f"\n{self.tab}# Connections\n") + for net in self.netlist.nets: + conn = self.net_to_skidl(net, sheet) + if conn: + code.append(conn) + + code.append(f"{self.tab}return\n") + + return "".join(code) + def create_main_file(self, output_dir: str): """Create the main.py file.""" code = [ @@ -401,7 +533,7 @@ def create_main_file(self, output_dir: str): "from skidl import *\n" ] - # Import only top-level modules (no parents) + # Import only top-level modules for sheet in self.sheets.values(): if not sheet.parent and sheet.name != 'main': module_name = self.legalize_name(sheet.name) @@ -409,33 +541,20 @@ def create_main_file(self, output_dir: str): code.extend([ "\ndef main():\n", - f"{self.tab}# Create nets\n", ]) - # Create all global nets - global_nets = set() - for sheet in self.sheets.values(): - if not sheet.parent: # Only include nets from top-level sheets - global_nets.update(sheet.imported_nets) - global_nets.update(sheet.local_nets) - - for net in sorted(global_nets): - if not net.startswith('unconnected'): - original_name = net - legal_name = self.legalize_name(original_name) - code.append(f"{self.tab}{legal_name} = Net('{original_name}')\n") - # Call only top-level subcircuits code.append(f"\n{self.tab}# Create subcircuits\n") for sheet in self.get_hierarchical_order(): - if not sheet.parent and sheet.name != 'main': # Only call top-level subcircuits + if not sheet.parent and sheet.name != 'main': + func_name = self.legalize_name(sheet.name) params = [] + + # Only pass nets that are actually used in the sheet for net in sorted(sheet.imported_nets): if not net.startswith('unconnected'): params.append(self.legalize_name(net)) - if 'GND' not in params: - params.append('GND') - func_name = self.legalize_name(sheet.name) + code.append(f"{self.tab}{func_name}({', '.join(params)})\n") code.extend([ @@ -447,6 +566,7 @@ def create_main_file(self, output_dir: str): main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) + def get_hierarchical_order(self): """Return sheets in dependency order.""" ordered = [] From 6cb2ee7a9513f1723299f7956f0db9e2c428fbfe Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 11:45:02 -0800 Subject: [PATCH 37/73] making progress! --- src/skidl/netlist_to_skidl.py | 187 +++++++++++++--------------------- 1 file changed, 73 insertions(+), 114 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index ab7418c0b..0b26ff6a3 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -356,19 +356,30 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append("\n@subcircuit\n") - # Function parameters - for hierarchical sheets, include both imported and local nets. - nets_to_pass = set(sheet.imported_nets) - if sheet.parent: - nets_to_pass.update(sheet.local_nets) - params = [] - for net in sorted(nets_to_pass): - if not net.startswith('unconnected'): - params.append(self.legalize_name(net)) - if 'GND' not in params: - params.append('GND') + # Function parameters - collect nets that are actually used in this sheet + used_nets = set() + + # Only add nets if the sheet has components or children + if sheet.components or sheet.children: + for net in sorted(sheet.imported_nets): + if not net.startswith('unconnected'): + # Check if net is actually used in this sheet + if sheet.name in self.net_usage.get(net, {}): + used_nets.add(self.legalize_name(net)) + # Add local nets if this is not the top level + if sheet.parent: + for net in sorted(sheet.local_nets): + if not net.startswith('unconnected'): + if sheet.name in self.net_usage.get(net, {}): + used_nets.add(self.legalize_name(net)) + + # Only include GND if it's actually used in this sheet + if 'GND' in self.net_usage and sheet.name in self.net_usage['GND']: + used_nets.add('GND') + func_name = self.legalize_name(sheet.name) - code.append(f"def {func_name}({', '.join(params)}):\n") + code.append(f"def {func_name}({', '.join(sorted(used_nets))}):\n") # Components if sheet.components: @@ -379,7 +390,7 @@ def generate_sheet_code(self, sheet: Sheet) -> str: # Local nets local_nets = sorted(net for net in sheet.local_nets - if not net.startswith('unconnected')) + if not net.startswith('unconnected')) if local_nets: code.append(f"{self.tab}# Local nets\n") for net in local_nets: @@ -393,22 +404,29 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append(f"\n{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: child_sheet = self.sheets[child_path] - func_name = self.legalize_name(child_sheet.name) - params = [] - # Pass through the parent's nets to the child + child_func_name = self.legalize_name(child_sheet.name) + child_params = [] + + # Only pass nets that the child sheet actually uses for net in sorted(child_sheet.imported_nets): if not net.startswith('unconnected'): - params.append(self.legalize_name(net)) - if 'GND' not in params: - params.append('GND') - code.append(f"{self.tab}{func_name}({', '.join(params)})\n") + if child_sheet.name in self.net_usage.get(net, {}): + child_params.append(self.legalize_name(net)) + + # Only pass GND if child uses it + if 'GND' in self.net_usage and child_sheet.name in self.net_usage['GND']: + child_params.append('GND') + + code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") # Connections - code.append(f"\n{self.tab}# Connections\n") - for net in self.netlist.nets: - conn = self.net_to_skidl(net, sheet) - if conn: - code.append(conn) + if sheet.components: + code.append(f"\n{self.tab}# Connections\n") + for net in self.netlist.nets: + conn = self.net_to_skidl(net, sheet) + if conn: + code.append(conn) + code.append(f"{self.tab}return\n") return "".join(code) @@ -444,87 +462,6 @@ def _update_sheet_net_classifications(self, net_hierarchy): print(f" - Imported net (path): {net_name}") break - def create_sheet_code(self, sheet: Sheet) -> str: - """Generate SKiDL code for a sheet.""" - code = [ - "# -*- coding: utf-8 -*-\n", - "from skidl import *\n" - ] - - # Import child subcircuits - for child_path in sheet.children: - child_sheet = self.sheets[child_path] - module_name = self.legalize_name(child_sheet.name) - code.append(f"from {module_name} import {module_name}\n") - - code.append("\n@subcircuit\n") - - # Function parameters - collect all nets this sheet needs - nets_to_pass = set() - - # Include imported nets - nets_to_pass.update(sheet.imported_nets) - - # For sheets that create nets used by children, include those too - if sheet.children: - for net_name, hierarchy in self.net_hierarchy.items(): - if hierarchy['origin_sheet'] == sheet.name: - nets_to_pass.add(net_name) - - # Remove unconnected nets and create parameter list - params = [] - for net in sorted(nets_to_pass): - if not net.startswith('unconnected'): - params.append(self.legalize_name(net)) - - func_name = self.legalize_name(sheet.name) - code.append(f"def {func_name}({', '.join(params)}):\n") - - # Components - if sheet.components: - code.append(f"{self.tab}# Components\n") - for comp in sorted(sheet.components, key=lambda x: x.ref): - code.append(self.component_to_skidl(comp)) - code.append("\n") - - # Create local nets - local_nets = sorted(net for net in sheet.local_nets - if not net.startswith('unconnected')) - if local_nets: - code.append(f"{self.tab}# Local nets\n") - for net in local_nets: - original_name = net - legal_name = self.legalize_name(original_name) - code.append(f"{self.tab}{legal_name} = Net('{original_name}')\n") - code.append("\n") - - # Hierarchical subcircuits - if sheet.children: - code.append(f"\n{self.tab}# Hierarchical subcircuits\n") - for child_path in sheet.children: - child_sheet = self.sheets[child_path] - func_name = self.legalize_name(child_sheet.name) - - # Build list of nets needed by child - child_nets = [] - - # Include nets that are actually used in child - for net in sorted(child_sheet.imported_nets): - if not net.startswith('unconnected'): - child_nets.append(self.legalize_name(net)) - - code.append(f"{self.tab}{func_name}({', '.join(child_nets)})\n") - - # Connections - code.append(f"\n{self.tab}# Connections\n") - for net in self.netlist.nets: - conn = self.net_to_skidl(net, sheet) - if conn: - code.append(conn) - - code.append(f"{self.tab}return\n") - - return "".join(code) def create_main_file(self, output_dir: str): """Create the main.py file.""" @@ -543,19 +480,41 @@ def create_main_file(self, output_dir: str): "\ndef main():\n", ]) + # Only create GND net if any top-level sheets actually use it + needs_gnd = False + for sheet in self.sheets.values(): + if not sheet.parent and sheet.name != 'main': + if 'GND' in self.net_usage and sheet.name in self.net_usage['GND']: + needs_gnd = True + break + + if needs_gnd: + code.append(f"{self.tab}# Create global ground net\n") + code.append(f"{self.tab}gnd = Net('GND')\n\n") + # Call only top-level subcircuits - code.append(f"\n{self.tab}# Create subcircuits\n") + code.append(f"{self.tab}# Create subcircuits\n") for sheet in self.get_hierarchical_order(): - if not sheet.parent and sheet.name != 'main': + if not sheet.parent and sheet.name != 'main': func_name = self.legalize_name(sheet.name) - params = [] - # Only pass nets that are actually used in the sheet - for net in sorted(sheet.imported_nets): - if not net.startswith('unconnected'): - params.append(self.legalize_name(net)) + # Only process sheets that have components or children + if sheet.components or sheet.children: + # Only include nets that are actually used in the sheet + used_nets = [] + for net in sorted(sheet.imported_nets): + if not net.startswith('unconnected'): + if sheet.name in self.net_usage.get(net, {}): + used_nets.append(self.legalize_name(net)) + + # Only add GND if sheet actually uses it + if 'GND' in self.net_usage and sheet.name in self.net_usage['GND']: + used_nets.append('gnd') - code.append(f"{self.tab}{func_name}({', '.join(params)})\n") + code.append(f"{self.tab}{func_name}({', '.join(used_nets)})\n") + else: + # Empty sheet with no components or children + code.append(f"{self.tab}{func_name}()\n") code.extend([ "\nif __name__ == \"__main__\":\n", @@ -566,7 +525,7 @@ def create_main_file(self, output_dir: str): main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) - + def get_hierarchical_order(self): """Return sheets in dependency order.""" ordered = [] From a632de3571dd515bb17dc314b1af638872fbb8ea Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 11:56:40 -0800 Subject: [PATCH 38/73] working net creation --- src/skidl/netlist_to_skidl.py | 73 +++++++---------------------------- 1 file changed, 15 insertions(+), 58 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 0b26ff6a3..afbdb76a8 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -341,6 +341,7 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" + def generate_sheet_code(self, sheet: Sheet) -> str: """Generate SKiDL code for a sheet.""" code = [ @@ -356,30 +357,20 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append("\n@subcircuit\n") - # Function parameters - collect nets that are actually used in this sheet - used_nets = set() - - # Only add nets if the sheet has components or children - if sheet.components or sheet.children: + # Function parameters - ONLY include imported nets + params = [] + if sheet.imported_nets: for net in sorted(sheet.imported_nets): if not net.startswith('unconnected'): - # Check if net is actually used in this sheet - if sheet.name in self.net_usage.get(net, {}): - used_nets.add(self.legalize_name(net)) - - # Add local nets if this is not the top level - if sheet.parent: - for net in sorted(sheet.local_nets): - if not net.startswith('unconnected'): - if sheet.name in self.net_usage.get(net, {}): - used_nets.add(self.legalize_name(net)) + params.append(self.legalize_name(net)) - # Only include GND if it's actually used in this sheet - if 'GND' in self.net_usage and sheet.name in self.net_usage['GND']: - used_nets.add('GND') - func_name = self.legalize_name(sheet.name) - code.append(f"def {func_name}({', '.join(sorted(used_nets))}):\n") + code.append(f"def {func_name}({', '.join(params)}):\n") + + # If there are no components, children, or nets, just add return + if not (sheet.components or sheet.children or sheet.local_nets): + code.append(f"{self.tab}return\n") + return "".join(code) # Components if sheet.components: @@ -401,22 +392,18 @@ def generate_sheet_code(self, sheet: Sheet) -> str: # Hierarchical subcircuits if sheet.children: - code.append(f"\n{self.tab}# Hierarchical subcircuits\n") + code.append(f"{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: child_sheet = self.sheets[child_path] child_func_name = self.legalize_name(child_sheet.name) child_params = [] - # Only pass nets that the child sheet actually uses + # Only pass imported nets that the child sheet actually uses for net in sorted(child_sheet.imported_nets): if not net.startswith('unconnected'): if child_sheet.name in self.net_usage.get(net, {}): child_params.append(self.legalize_name(net)) - # Only pass GND if child uses it - if 'GND' in self.net_usage and child_sheet.name in self.net_usage['GND']: - child_params.append('GND') - code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") # Connections @@ -427,41 +414,11 @@ def generate_sheet_code(self, sheet: Sheet) -> str: if conn: code.append(conn) + # Always add return statement at the end code.append(f"{self.tab}return\n") - + return "".join(code) - def _update_sheet_net_classifications(self, net_hierarchy): - """Update sheet net classifications based on hierarchy analysis.""" - print("\n3. Updating Sheet Net Classifications:") - - for sheet in self.sheets.values(): - sheet.local_nets.clear() - sheet.imported_nets.clear() - - print(f"\nSheet: {sheet.name}") - for net_name, hierarchy in net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - - # If this sheet is the origin, it's a local net - if hierarchy['origin_sheet'] == sheet.name: - sheet.local_nets.add(net_name) - print(f" - Local net: {net_name}") - - # If used in this sheet but originates elsewhere, it's imported - elif sheet.name in hierarchy['used_in_sheets']: - sheet.imported_nets.add(net_name) - print(f" - Imported net: {net_name}") - - # If this sheet is in the path for any child, it needs to pass the net - else: - for path in hierarchy['path_to_children']: - if sheet.name in path: - sheet.imported_nets.add(net_name) - print(f" - Imported net (path): {net_name}") - break - def create_main_file(self, output_dir: str): """Create the main.py file.""" From e865a2da31805ab3fab25d87d4a961f87d6dbfc5 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sat, 1 Feb 2025 12:24:06 -0800 Subject: [PATCH 39/73] remove prints, change some comments --- src/skidl/netlist_to_skidl.py | 74 +++++++++++++---------------------- 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index afbdb76a8..b8ccfae80 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -11,6 +11,7 @@ from dataclasses import dataclass from typing import Dict, List, Set from kinparse import parse_netlist +from .logger import active_logger @dataclass class Sheet: @@ -35,7 +36,8 @@ def __init__(self, netlist_src): def extract_sheet_info(self): """Build sheet hierarchy from netlist.""" - print("\n=== Extracting Sheet Info ===") + active_logger.info("=== Extracting Sheet Info ===") + # First pass: Create all sheets for sheet in self.netlist.sheets: path = sheet.name.strip('/') @@ -69,7 +71,7 @@ def get_sheet_path(self, comp): def assign_components_to_sheets(self): """Assign components to their respective sheets.""" - print("\n=== Assigning Components to Sheets ===") + active_logger.info("=== Assigning Components to Sheets ===") for comp in self.netlist.parts: sheet_name = self.get_sheet_path(comp) if sheet_name: @@ -107,18 +109,12 @@ def find_lowest_common_ancestor(self, sheet1, sheet2): def find_optimal_net_origin(self, used_sheets, net_name): """ Determine the optimal sheet to create a net based on hierarchy. - - Args: - used_sheets: Set of sheets where net has component pins - net_name: Name of net being analyzed - Returns: - (origin_sheet, paths_to_children) """ # Single sheet usage - create in that sheet if len(used_sheets) == 1: return list(used_sheets)[0], [] - # Power nets (+3.3V, +5V, GND, Vmotor) belong in Project Architecture + # Power nets belong in Project Architecture if any(pwr in net_name for pwr in ['+3.3V', '+5V', 'GND', 'Vmotor']): return 'Project Architecture', [list(used_sheets)] @@ -177,28 +173,28 @@ def find_optimal_net_origin(self, used_sheets, net_name): def analyze_nets(self): """Analyze nets to determine hierarchy and relationships between sheets.""" - print("\n=== Starting Enhanced Net Analysis ===") + active_logger.info("=== Starting Net Analysis ===") # Data structures for analysis net_usage = defaultdict(lambda: defaultdict(set)) # net -> sheet -> pins net_hierarchy = {} # net -> {origin_sheet, used_in_sheets, path_to_children} - print("\n1. Mapping Net Usage Across Sheets:") + active_logger.info("1. Mapping Net Usage Across Sheets:") # First pass: Build net usage map for net in self.netlist.nets: - print(f"\nAnalyzing net: {net.name}") + active_logger.debug(f"\nAnalyzing net: {net.name}") for pin in net.pins: for comp in self.netlist.parts: if comp.ref == pin.ref: sheet_name = self.get_sheet_path(comp) if sheet_name: net_usage[net.name][sheet_name].add(f"{comp.ref}.{pin.num}") - print(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") - print("\n2. Analyzing Net Origins and Hierarchy:") + active_logger.info("2. Analyzing Net Origins and Hierarchy:") # Second pass: Determine net origins and build hierarchy for net_name, sheet_usages in net_usage.items(): - print(f"\nNet: {net_name}") + active_logger.debug(f"\nNet: {net_name}") used_sheets = set(sheet_usages.keys()) # Find optimal origin and paths @@ -210,16 +206,16 @@ def analyze_nets(self): 'path_to_children': paths_to_children } - print(f" - Origin sheet: {origin_sheet}") - print(f" - Used in sheets: {used_sheets}") - print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") + active_logger.debug(f" - Origin sheet: {origin_sheet}") + active_logger.debug(f" - Used in sheets: {used_sheets}") + active_logger.debug(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") # Third pass: Update sheet net classifications for sheet in self.sheets.values(): sheet.local_nets.clear() sheet.imported_nets.clear() - print(f"\nSheet: {sheet.name}") + active_logger.debug(f"\nSheet: {sheet.name}") for net_name, hierarchy in net_hierarchy.items(): if net_name.startswith('unconnected'): continue @@ -227,47 +223,31 @@ def analyze_nets(self): # If this sheet is the origin, it's a local net if hierarchy['origin_sheet'] == sheet.name: sheet.local_nets.add(net_name) - print(f" - Local net: {net_name}") + active_logger.debug(f" - Local net: {net_name}") # If used in this sheet but originates elsewhere, it's imported elif sheet.name in hierarchy['used_in_sheets']: sheet.imported_nets.add(net_name) - print(f" - Imported net: {net_name}") + active_logger.debug(f" - Imported net: {net_name}") # If this sheet is in the path between origin and users, it needs to pass the net else: for path in hierarchy['path_to_children']: if sheet.name in path: sheet.imported_nets.add(net_name) - print(f" - Imported net (path): {net_name}") + active_logger.debug(f" - Imported net (path): {net_name}") break # Store the analysis results for use in code generation self.net_hierarchy = net_hierarchy self.net_usage = net_usage - - self._print_net_summary() - - def _print_net_summary(self): - """Print a summary of net analysis results.""" - print("\n=== Net Analysis Complete ===") - print("\nNet Origin Summary:") - for net_name, hierarchy in self.net_hierarchy.items(): - if not net_name.startswith('unconnected'): - print(f"\nNet: {net_name}") - print(f" Origin: {hierarchy['origin_sheet']}") - print(f" Used in: {hierarchy['used_in_sheets']}") - if hierarchy['path_to_children']: - for path in hierarchy['path_to_children']: - print(f" Path: {' -> '.join(path)}") - + def legalize_name(self, name: str, is_filename: bool = False) -> str: - """Convert any name into a valid Python identifier. - Handles leading and trailing +/- with _p and _n suffixes/prefixes.""" + """Convert any name into a valid Python identifier.""" # Remove leading slashes and spaces name = name.lstrip('/ ') - # Handle trailing + or - first + # Handle trailing + or - if name.endswith('+'): name = name[:-1] + '_p' elif name.endswith('-'): @@ -341,7 +321,6 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" - def generate_sheet_code(self, sheet: Sheet) -> str: """Generate SKiDL code for a sheet.""" code = [ @@ -381,7 +360,7 @@ def generate_sheet_code(self, sheet: Sheet) -> str: # Local nets local_nets = sorted(net for net in sheet.local_nets - if not net.startswith('unconnected')) + if not net.startswith('unconnected')) if local_nets: code.append(f"{self.tab}# Local nets\n") for net in local_nets: @@ -419,7 +398,6 @@ def generate_sheet_code(self, sheet: Sheet) -> str: return "".join(code) - def create_main_file(self, output_dir: str): """Create the main.py file.""" code = [ @@ -482,7 +460,6 @@ def create_main_file(self, output_dir: str): main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) - def get_hierarchical_order(self): """Return sheets in dependency order.""" ordered = [] @@ -530,6 +507,7 @@ def convert(self, output_dir: str = None): if output_dir: os.makedirs(output_dir, exist_ok=True) + active_logger.info(f"Generating files in {output_dir}") # Generate all sheet files for sheet in self.sheets.values(): @@ -537,17 +515,19 @@ def convert(self, output_dir: str = None): filename = self.legalize_name(sheet.name, is_filename=True) + '.py' sheet_path = Path(output_dir) / filename sheet_path.write_text(self.generate_sheet_code(sheet)) - print(f"Wrote sheet file: {sheet_path}") + active_logger.debug(f"Created sheet file: {sheet_path}") # Create main.py last self.create_main_file(output_dir) + active_logger.info("Conversion completed successfully") else: main_sheet = next((s for s in self.sheets.values() if not s.parent), None) if main_sheet: return self.generate_sheet_code(main_sheet) return "" + def netlist_to_skidl(netlist_src: str, output_dir: str = None): """Convert a KiCad netlist to SKiDL Python files.""" converter = HierarchicalConverter(netlist_src) - return converter.convert(output_dir) + return converter.convert(output_dir) \ No newline at end of file From 34078ce1204758a20eac98ef9884ef0c664d0b0a Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 09:20:32 -0800 Subject: [PATCH 40/73] refactor kicad_skidl_llm.py script --- kicad_skidl_llm.py | 717 +++++++++++++++++++++++++++++++++------------ setup.py | 7 +- 2 files changed, 533 insertions(+), 191 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index d3a5d27b7..e7bf3962b 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -1,21 +1,379 @@ #!/usr/bin/env python3 +""" +KiCad-SKiDL-LLM Pipeline +------------------------ + +This script provides a flexible pipeline for working with KiCad schematics, +netlists, SKiDL projects, and LLM-based circuit analysis. It supports multiple +entry points and operation modes: + +Entry Points: + - KiCad schematic (.kicad_sch) + - Netlist (.net) + - SKiDL project (single file or directory) + +Operations: + - Generate netlist from KiCad schematic + - Convert netlist to SKiDL project + - Analyze circuits using LLM (local or cloud-based) + +Key Features: + - Safe discovery and analysis of SKiDL circuits using AST parsing + - Support for both single-file and multi-file SKiDL projects + - Flexible LLM backend selection (OpenRouter API or local Ollama) + - Comprehensive error handling and reporting + +Usage Examples: + # Generate netlist and SKiDL project from schematic + python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist --generate-skidl + + # Analyze existing SKiDL project using OpenRouter + python kicad_skidl_llm.py --skidl-source myproject/ --analyze --api-key YOUR_KEY + + # Generate SKiDL from netlist and analyze using Ollama + python kicad_skidl_llm.py --netlist circuit.net --generate-skidl --analyze --backend ollama + +Known Limitations: + 1. Memory usage may be high for projects with many subcircuits + 2. Python path handling may need adjustment for complex project structures + 3. Circular dependencies in SKiDL projects may cause issues + 4. File encoding issues possible with non-UTF8 files + 5. Large projects may hit API rate limits with cloud LLMs + +Dependencies: + - Python 3.7+ + - KiCad 7.0+ (for schematic operations) + - SKiDL + - AST (standard library) + - Either OpenRouter API key or local Ollama installation + +Author: [Your Name] +Date: February 2024 +License: MIT +""" + import os import sys +import ast +import inspect import subprocess import importlib +import textwrap +import traceback from pathlib import Path +from typing import List, Dict, Optional, Union, Tuple import argparse from skidl import * -def parse_args(): - """Parse command line arguments.""" + +class CircuitDiscoveryError(Exception): + """Raised when there are issues discovering or loading circuits.""" + pass + + +class CircuitAnalyzer: + """ + Handles discovery and analysis of SKiDL circuits. + + This class provides methods for: + - Finding @subcircuit decorated functions in Python files + - Safely loading and executing discovered circuits + - Running LLM analysis on circuits + + The analyzer uses AST parsing to discover circuits without executing + potentially unsafe code, then creates isolated environments for + circuit execution and analysis. + """ + + @staticmethod + def find_subcircuits(directory: Path) -> List[Dict[str, str]]: + """ + Recursively find all functions decorated with @subcircuit in Python files. + + Uses AST parsing to safely discover circuits without executing code. + Skips files that can't be parsed and logs warnings. + + Args: + directory: Root directory to search for Python files + + Returns: + List of dicts containing: + - 'file': Path to Python file + - 'function': Name of decorated function + + Raises: + CircuitDiscoveryError: If no valid Python files found + """ + subcircuits = [] + + class SubcircuitFinder(ast.NodeVisitor): + """AST visitor that identifies @subcircuit decorated functions.""" + def visit_FunctionDef(self, node): + for decorator in node.decorator_list: + if isinstance(decorator, ast.Name) and decorator.id == 'subcircuit': + subcircuits.append({ + 'file': str(current_file), + 'function': node.name, + 'lineno': node.lineno + }) + + python_files = list(directory.rglob('*.py')) + if not python_files: + raise CircuitDiscoveryError(f"No Python files found in {directory}") + + for current_file in python_files: + try: + with open(current_file, 'r', encoding='utf-8') as f: + tree = ast.parse(f.read()) + SubcircuitFinder().visit(tree) + except Exception as e: + print(f"Warning: Could not parse {current_file}: {e}") + + return subcircuits + + @staticmethod + def create_analysis_module( + subcircuits: List[Dict[str, str]], + output_dir: Path + ) -> Tuple[Path, str]: + """ + Create a temporary Python module that imports and executes discovered subcircuits. + + Args: + subcircuits: List of subcircuit information from find_subcircuits() + output_dir: Directory to write temporary module + + Returns: + Tuple of: + - Path to created module + - Generated code string (for debugging) + + Raises: + CircuitDiscoveryError: If module creation fails + """ + analysis_file = output_dir / 'circuit_analysis.py' + + try: + # Generate imports and function calls + code_lines = [ + "from skidl import *\n", + "from skidl.tools import *\n" + ] + + # Import each unique file containing subcircuits + unique_files = {s['file'] for s in subcircuits} + for file in unique_files: + module_name = Path(file).stem + code_lines.append(f"from {module_name} import *\n") + + # Create main function to execute subcircuits + code_lines.append("\ndef main():\n") + for circuit in subcircuits: + code_lines.append(f" {circuit['function']}()\n") + + code = ''.join(code_lines) + + with open(analysis_file, 'w', encoding='utf-8') as f: + f.write(code) + + return analysis_file, code + + except Exception as e: + raise CircuitDiscoveryError( + f"Failed to create analysis module: {str(e)}" + ) from e + + @staticmethod + def analyze_circuits( + source: Path, + output_file: str, + api_key: Optional[str] = None, + backend: str = 'openrouter', + model: Optional[str] = None, + prompt: Optional[str] = None + ) -> dict: + """ + Analyze circuits from a SKiDL source (file or directory). + + Handles both single file and project directory cases: + - Single file: Imports and executes directly + - Directory: Finds @subcircuit functions and creates analysis module + + Args: + source: Path to SKiDL file or project directory + output_file: Where to save analysis results + api_key: API key for cloud LLM service + backend: 'openrouter' or 'ollama' + model: Model name for selected backend + prompt: Custom analysis prompt + + Returns: + Analysis results dictionary containing: + - success: bool + - subcircuits: Dict of subcircuit analysis results + - total_time_seconds: float + - total_tokens: int (if applicable) + - error: str (if success is False) + + Raises: + CircuitDiscoveryError: For circuit loading/execution issues + RuntimeError: For analysis failures + """ + if source.is_file(): + # Single file case - import directly + sys.path.insert(0, str(source.parent)) + try: + module = importlib.import_module(source.stem) + importlib.reload(module) + if hasattr(module, 'main'): + module.main() + else: + raise CircuitDiscoveryError( + f"No main() function found in {source}" + ) + except Exception as e: + raise CircuitDiscoveryError( + f"Failed to load/execute {source}: {str(e)}" + ) from e + finally: + sys.path.pop(0) + + else: + # Directory case - find and analyze subcircuits + try: + subcircuits = CircuitAnalyzer.find_subcircuits(source) + if not subcircuits: + raise CircuitDiscoveryError( + f"No @subcircuit functions found in {source}" + ) + + analysis_module, generated_code = CircuitAnalyzer.create_analysis_module( + subcircuits, source + ) + + print(f"Found {len(subcircuits)} circuits to analyze:") + for circuit in subcircuits: + print(f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})") + + sys.path.insert(0, str(source)) + try: + module = importlib.import_module('circuit_analysis') + importlib.reload(module) + module.main() + finally: + sys.path.pop(0) + try: + analysis_module.unlink() # Clean up temporary file + except Exception as e: + print(f"Warning: Failed to remove temporary module: {e}") + + except Exception as e: + raise CircuitDiscoveryError( + f"Circuit discovery/execution failed: {str(e)}" + ) from e + + # Run LLM analysis + try: + analysis_kwargs = { + 'output_file': output_file, + 'backend': backend, + 'analyze_subcircuits': True + } + + if api_key: + analysis_kwargs['api_key'] = api_key + if model: + analysis_kwargs['model'] = model + if prompt: + analysis_kwargs['custom_prompt'] = prompt + + return default_circuit.analyze_with_llm(**analysis_kwargs) + + except Exception as e: + raise RuntimeError(f"LLM analysis failed: {str(e)}") from e + + +def validate_kicad_cli(path: str) -> str: + """ + Validate that KiCad CLI exists and is executable. + + Args: + path: Path to kicad-cli executable + + Returns: + Validated path + + Raises: + FileNotFoundError: If executable not found + PermissionError: If executable lacks permissions + """ + cli_path = Path(path) + if not cli_path.exists(): + raise FileNotFoundError(f"KiCad CLI not found: {path}") + if not os.access(str(cli_path), os.X_OK): + raise PermissionError(f"KiCad CLI not executable: {path}") + return str(cli_path) + + +def parse_args() -> argparse.Namespace: + """ + Parse and validate command line arguments. + + Returns: + Parsed argument namespace + + Raises: + argparse.ArgumentError: For invalid argument combinations + """ parser = argparse.ArgumentParser( - description='KiCad schematic to SKiDL conversion pipeline' + description='KiCad schematic to SKiDL conversion and analysis pipeline', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=textwrap.dedent(""" + Examples: + # Generate netlist and SKiDL project from schematic + %(prog)s --schematic design.kicad_sch --generate-netlist --generate-skidl + + # Analyze existing SKiDL project using OpenRouter + %(prog)s --skidl-source myproject/ --analyze --api-key YOUR_KEY + + # Generate SKiDL from netlist and analyze using Ollama + %(prog)s --netlist circuit.net --generate-skidl --analyze --backend ollama + """) ) - parser.add_argument( - '--schematic', '-s', required=True, + + # Input source group (mutually exclusive) + source_group = parser.add_mutually_exclusive_group(required=True) + source_group.add_argument( + '--schematic', '-s', help='Path to KiCad schematic (.kicad_sch) file' ) + source_group.add_argument( + '--netlist', '-n', + help='Path to netlist (.net) file' + ) + source_group.add_argument( + '--skidl-source', + help='Path to SKiDL file or project directory' + ) + + # Operation mode flags + parser.add_argument( + '--generate-netlist', + action='store_true', + help='Generate netlist from schematic' + ) + parser.add_argument( + '--generate-skidl', + action='store_true', + help='Generate SKiDL project from netlist' + ) + parser.add_argument( + '--analyze', + action='store_true', + help='Run LLM analysis on circuits' + ) + + # Optional configuration parser.add_argument( '--kicad-cli', default="/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", @@ -24,16 +382,21 @@ def parse_args(): parser.add_argument( '--output-dir', '-o', default='.', - help="Output directory for generated files. If set to '.', a default directory named _SKIDL is created." + help='Output directory for generated files' ) parser.add_argument( - '--analyze', - action='store_true', - help='Run LLM analysis on the generated SKiDL circuit' + '--api-key', + help='OpenRouter API key for cloud LLM analysis' ) parser.add_argument( - '--api-key', - help='OpenRouter API key for LLM analysis' + '--backend', + choices=['openrouter', 'ollama'], + default='openrouter', + help='LLM backend to use' + ) + parser.add_argument( + '--model', + help='LLM model name for selected backend' ) parser.add_argument( '--analysis-output', @@ -41,195 +404,171 @@ def parse_args(): help='Output file for analysis results' ) parser.add_argument( - '--model', - default='google/gemini-flash-1.5', - help='LLM model to use for analysis (default: google/gemini-flash-1.5)' + '--analysis-prompt', + help='Custom prompt for circuit analysis' ) - return parser.parse_args() - -def execute_skidl_main(skidl_dir: Path) -> None: - """ - Import and execute the main() function from the generated SKiDL project. - This creates the circuit that we can then analyze. - """ - # Add project directory to Python path - sys.path.insert(0, str(skidl_dir)) - try: - # Import the generated main module - import main - importlib.reload(main) # Reload in case it was previously imported - - # Execute main() to create the circuit - main.main() - - finally: - # Clean up sys.path - sys.path.pop(0) - -def analyze_skidl_circuit(output_file: str, api_key: str = None, model: str = 'google/gemini-flash-1.5') -> dict: - """ - Analyze the current default circuit using SKiDL's built-in analyze_with_llm. + args = parser.parse_args() - Args: - output_file: Path to save analysis results - api_key: OpenRouter API key (if None, will try to get from environment) - model: LLM model to use for analysis (default: google/gemini-flash-1.5) + # Validate argument combinations + if args.generate_netlist and not args.schematic: + parser.error("--generate-netlist requires --schematic") + if args.generate_skidl and not (args.netlist or args.generate_netlist): + parser.error("--generate-skidl requires --netlist or --generate-netlist") + if args.analyze and args.backend == 'openrouter' and not args.api_key: + parser.error("OpenRouter backend requires --api-key") - Returns: - dict: Analysis results including success status, timing, and any errors - """ - # Get API key from environment if not provided - if api_key is None: - api_key = os.getenv("OPENROUTER_API_KEY") - if not api_key: - return { - "success": False, - "error": "OpenRouter API key not found. Please provide via --api-key or set OPENROUTER_API_KEY environment variable." - } + return args - try: - # First save query only to get circuit description - default_circuit.analyze_with_llm( - output_file="query.txt", - save_query_only=True - ) - - # Then do the actual analysis with the specified model - return default_circuit.analyze_with_llm( - api_key=api_key, - output_file=output_file, - analyze_subcircuits=True, - model=model, - custom_prompt="Analyze the circuit for functionality, safety considerations, and potential improvements." - ) - except Exception as e: - return { - "success": False, - "error": f"Analysis failed: {str(e)}" - } - -def validate_schematic(schematic_path: Path) -> Path: - """Validate the schematic file exists and has the correct extension.""" - if not schematic_path.exists(): - raise FileNotFoundError(f"Schematic file not found: {schematic_path}") - if schematic_path.suffix != '.kicad_sch': - raise ValueError(f"Input file must be a .kicad_sch file: {schematic_path}") - return schematic_path - -def generate_netlist(schematic_path: Path, kicad_cli_path: str, output_dir: Path) -> Path: - """Generate netlist from KiCad schematic in the output directory.""" - netlist_path = output_dir / f"{schematic_path.stem}.net" - cmd = [ - kicad_cli_path, - 'sch', - 'export', - 'netlist', - '-o', - str(netlist_path), - str(schematic_path) - ] - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"Generated netlist: {netlist_path}") - return netlist_path - except subprocess.CalledProcessError as e: - print(f"Error generating netlist:\n{e.stderr}", file=sys.stderr) - raise - -def generate_skidl_project(netlist_path: Path, output_dir: Path) -> Path: - """Generate SKiDL project from the netlist in the output directory.""" - skidl_dir = output_dir - skidl_dir.mkdir(parents=True, exist_ok=True) - - # Run netlist_to_skidl to generate the SKiDL files - cmd = [ - 'netlist_to_skidl', - '-i', str(netlist_path), - '--output', str(skidl_dir) - ] - try: - subprocess.run(cmd, check=True, capture_output=True, text=True) - print("\nGenerated SKiDL project files:") - for file_path in sorted(skidl_dir.glob("*.py")): - print(f" {file_path.name}") - return skidl_dir - except subprocess.CalledProcessError as e: - print(f"Error generating SKiDL project:\n{e.stderr}", file=sys.stderr) - raise def main(): - args = parse_args() - schematic_path = validate_schematic(Path(args.schematic)) - - # Determine the output directory. - if args.output_dir == '.': - output_dir = Path(f"{schematic_path.stem}_SKIDL") - else: - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - print(f"Found valid schematic: {schematic_path}") + """ + Main execution function implementing the KiCad-SKiDL-LLM pipeline. - # Generate the netlist. - netlist_path = generate_netlist(schematic_path, args.kicad_cli, output_dir) - print(f"Successfully generated netlist at: {netlist_path}") + Implements a flexible pipeline that can: + 1. Generate netlist from KiCad schematic + 2. Convert netlist to SKiDL project + 3. Analyze circuits using LLM - # Convert the netlist to a SKiDL project. - skidl_dir = generate_skidl_project(netlist_path, output_dir) - print(f"Successfully generated SKiDL project at: {skidl_dir}") + The pipeline tracks state between steps and provides detailed + error reporting for each stage. - # Clean up the temporary netlist file. - # try: - # netlist_path.unlink() - # except Exception as e: - # print(f"Warning: could not remove netlist file: {e}", file=sys.stderr) - - # Run LLM analysis if requested - if args.analyze: - print("\nRunning circuit analysis...") - try: - # First execute the main script to create the circuit - execute_skidl_main(skidl_dir) - - # Print docstrings for each subcircuit before analysis - print("\nSubcircuit Docstrings:") - for name, doc in default_circuit.subcircuit_docs.items(): - print(f"\n{name}:") - print(doc) - - # Run the analysis - results = analyze_skidl_circuit( - output_file=args.analysis_output, - api_key=args.api_key, - model=args.model - ) - - if results["success"]: - print("\nAnalysis Results:") - for hier, analysis in results["subcircuits"].items(): - print(f"\nSubcircuit: {hier}") - if analysis["success"]: - print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) - if tokens: - print(f"Tokens used: {tokens}") - else: - print(f"Analysis failed: {analysis['error']}") + Raises: + Various exceptions with descriptive messages for different + failure modes. + """ + try: + args = parse_args() + + # Determine output directory + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Track current state for pipeline + current_netlist = None + current_skidl = None + + # 1. Generate netlist if requested + if args.schematic and args.generate_netlist: + print("\nStep 1: Generating netlist from schematic...") + schematic_path = Path(args.schematic) + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic not found: {schematic_path}") + if schematic_path.suffix != '.kicad_sch': + raise ValueError(f"Input must be .kicad_sch file: {schematic_path}") + + # Validate KiCad CLI + kicad_cli = validate_kicad_cli(args.kicad_cli) + + # Generate netlist + netlist_path = output_dir / f"{schematic_path.stem}.net" + try: + subprocess.run([ + kicad_cli, + 'sch', + 'export', + 'netlist', + '-o', + str(netlist_path), + str(schematic_path) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"Netlist generation failed:\n{e.stderr}" + ) from e + + current_netlist = netlist_path + print(f"✓ Generated netlist: {netlist_path}") + + # 2. Start from netlist if provided + elif args.netlist: + current_netlist = Path(args.netlist) + if not current_netlist.exists(): + raise FileNotFoundError(f"Netlist not found: {current_netlist}") + print(f"\nUsing existing netlist: {current_netlist}") + + # 3. Generate SKiDL project if requested + if current_netlist and args.generate_skidl: + print("\nStep 2: Generating SKiDL project from netlist...") + skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" + skidl_dir.mkdir(parents=True, exist_ok=True) + + try: + subprocess.run([ + 'netlist_to_skidl', + '-i', str(current_netlist), + '--output', str(skidl_dir) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError( + f"SKiDL project generation failed:\n{e.stderr}" + ) from e + + current_skidl = skidl_dir + print(f"✓ Generated SKiDL project: {skidl_dir}") + + # 4. Start from SKiDL if provided + elif args.skidl_source: + current_skidl = Path(args.skidl_source) + if not current_skidl.exists(): + raise FileNotFoundError(f"SKiDL source not found: {current_skidl}") + print(f"\nUsing existing SKiDL source: {current_skidl}") + + # 5. Run analysis if requested + if args.analyze: + if not current_skidl: + raise ValueError("No SKiDL source available for analysis") - print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") - if results.get('total_tokens'): - print(f"Total tokens used: {results['total_tokens']}") - print(f"Analysis results saved to: {args.analysis_output}") - else: - print(f"\nAnalysis failed: {results.get('error', 'Unknown error')}") + print("\nStep 3: Analyzing circuits...") + try: + results = CircuitAnalyzer.analyze_circuits( + source=current_skidl, + output_file=args.analysis_output, + api_key=args.api_key, + backend=args.backend, + model=args.model, + prompt=args.analysis_prompt + ) - except KeyboardInterrupt: - print("\nAnalysis interrupted by user") - sys.exit(1) - except Exception as e: - print(f"\nError during analysis: {str(e)}", file=sys.stderr) - sys.exit(1) + if results["success"]: + print("\nAnalysis Results:") + for hier, analysis in results["subcircuits"].items(): + print(f"\nSubcircuit: {hier}") + if analysis["success"]: + print(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) + if tokens: + print(f" Tokens used: {tokens}") + else: + print(f"✗ Analysis failed: {analysis['error']}") + + print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + if results.get('total_tokens'): + print(f"Total tokens used: {results['total_tokens']}") + print(f"Analysis results saved to: {args.analysis_output}") + else: + raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") + + except Exception as e: + print("\n✗ Circuit analysis failed!") + print(f"Error: {str(e)}") + if args.backend == 'openrouter': + print("\nTroubleshooting tips:") + print("1. Check your API key") + print("2. Verify you have sufficient API credits") + print("3. Check for rate limiting") + else: + print("\nTroubleshooting tips:") + print("1. Verify Ollama is running locally") + print("2. Check if the requested model is installed") + raise + + except Exception as e: + print("\nError:", str(e)) + print("\nStack trace:") + traceback.print_exc() + sys.exit(1) + if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/setup.py b/setup.py index 632ad1d78..31addb6cc 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,15 @@ requirements = [ "future >= 0.15.0", "sexpdata == 1.0.0", - "kinparse >= 1.2.2", + "kinparse >= 1.2.3", "kinet2pcb >= 1.1.2", - #'PySpice; python_version >= "3.0"', "graphviz", "deprecation", "requests >= 2.31.0", + # New requirements for kicad_skidl_llm.py + "ast3", # For ast parsing of Python files + "importlib-metadata", # For importlib support + "typing-extensions", # For type hints in Python <3.8 ] test_requirements = [ From b1ab6b7dc62ec2a4780fc3b5042b01afcebdf318 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 09:58:30 -0800 Subject: [PATCH 41/73] refactor net building logic to look for top level to generate net --- setup.py | 1 - src/skidl/netlist_to_skidl.py | 129 ++++++++++++++++------------------ 2 files changed, 59 insertions(+), 71 deletions(-) diff --git a/setup.py b/setup.py index 31addb6cc..d4b1b8a74 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,6 @@ "deprecation", "requests >= 2.31.0", # New requirements for kicad_skidl_llm.py - "ast3", # For ast parsing of Python files "importlib-metadata", # For importlib support "typing-extensions", # For type hints in Python <3.8 ] diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index b8ccfae80..f209ff445 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -109,39 +109,19 @@ def find_lowest_common_ancestor(self, sheet1, sheet2): def find_optimal_net_origin(self, used_sheets, net_name): """ Determine the optimal sheet to create a net based on hierarchy. + Returns (origin_sheet, paths_to_children) where: + - origin_sheet is the sheet where the net should be defined + - paths_to_children are the paths for passing the net to child sheets """ # Single sheet usage - create in that sheet if len(used_sheets) == 1: return list(used_sheets)[0], [] - - # Power nets belong in Project Architecture - if any(pwr in net_name for pwr in ['+3.3V', '+5V', 'GND', 'Vmotor']): - return 'Project Architecture', [list(used_sheets)] - - # Get sheet paths + + # Get sheet paths for all usage points sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) for sheet in used_sheets} - - # Check if sheets are children of Project Architecture - if 'Project Architecture' in self.sheets: - shared_nets = 0 - for sheet in used_sheets: - path = sheet_paths.get(sheet, []) - if 'Project Architecture' in path: - shared_nets += 1 - - # If multiple sheets under Project Architecture share this net, - # it should be created in Project Architecture - if shared_nets > 1: - paths = [] - for sheet in used_sheets: - if sheet != 'Project Architecture': - path = self.get_path_between_sheets('Project Architecture', sheet) - if path: - paths.append(path) - return 'Project Architecture', paths - - # If not in Project Architecture, find lowest common parent + + # Find the common ancestors among all paths common_ancestors = None for paths in sheet_paths.values(): if common_ancestors is None: @@ -149,27 +129,26 @@ def find_optimal_net_origin(self, used_sheets, net_name): else: common_ancestors &= set(paths) - if common_ancestors: - lowest = max(common_ancestors, - key=lambda x: max(path.index(x) if x in path else -1 - for path in sheet_paths.values())) - - # Build paths from parent to children - paths = [] - for sheet in used_sheets: - if sheet != lowest: - path = sheet_paths[sheet] - start_idx = path.index(lowest) - child_path = path[start_idx:] - if len(child_path) > 1: - paths.append(child_path) - return lowest, paths - - # Default to Project Architecture for multi-sheet nets - if len(used_sheets) > 1 and 'Project Architecture' in self.sheets: - return 'Project Architecture', [list(used_sheets)] - - return list(used_sheets)[0], [] + if not common_ancestors: + # If no common ancestors, place at root level + return '', [] + + # Find highest common parent that contains all usage points + highest_common = min(common_ancestors, + key=lambda x: min(path.index(x) if x in path else float('inf') + for path in sheet_paths.values())) + + # Build paths from parent to children + paths = [] + for sheet in used_sheets: + if sheet != highest_common: + path = sheet_paths[sheet] + start_idx = path.index(highest_common) + child_path = path[start_idx:] + if len(child_path) > 1: # Only add if there's a path to traverse + paths.append(child_path) + + return highest_common, paths def analyze_nets(self): """Analyze nets to determine hierarchy and relationships between sheets.""" @@ -241,7 +220,7 @@ def analyze_nets(self): # Store the analysis results for use in code generation self.net_hierarchy = net_hierarchy self.net_usage = net_usage - + def legalize_name(self, name: str, is_filename: bool = False) -> str: """Convert any name into a valid Python identifier.""" # Remove leading slashes and spaces @@ -415,17 +394,22 @@ def create_main_file(self, output_dir: str): "\ndef main():\n", ]) - # Only create GND net if any top-level sheets actually use it - needs_gnd = False - for sheet in self.sheets.values(): - if not sheet.parent and sheet.name != 'main': - if 'GND' in self.net_usage and sheet.name in self.net_usage['GND']: - needs_gnd = True - break + # Find all nets that need to be created at the top level + top_level_nets = set() + for net_name, hierarchy in self.net_hierarchy.items(): + if not net_name.startswith('unconnected'): + # If net originates at root level or is used across multiple top-level sheets + if (not hierarchy['origin_sheet'] or + len([s for s in hierarchy['used_in_sheets'] if not self.sheets[s].parent]) > 1): + top_level_nets.add(net_name) - if needs_gnd: - code.append(f"{self.tab}# Create global ground net\n") - code.append(f"{self.tab}gnd = Net('GND')\n\n") + # Create all top-level nets + if top_level_nets: + code.append(f"{self.tab}# Create global nets\n") + for net_name in sorted(top_level_nets): + legal_name = self.legalize_name(net_name) + code.append(f"{self.tab}{legal_name} = Net('{net_name}')\n") + code.append("\n") # Call only top-level subcircuits code.append(f"{self.tab}# Create subcircuits\n") @@ -435,18 +419,22 @@ def create_main_file(self, output_dir: str): # Only process sheets that have components or children if sheet.components or sheet.children: - # Only include nets that are actually used in the sheet - used_nets = [] - for net in sorted(sheet.imported_nets): - if not net.startswith('unconnected'): - if sheet.name in self.net_usage.get(net, {}): - used_nets.append(self.legalize_name(net)) + # Get all nets that need to be passed to this sheet + needed_nets = [] + for net_name in sorted(sheet.imported_nets): + if not net_name.startswith('unconnected'): + # Only include nets that are actually used in this sheet or its children + sheet_path = self.get_sheet_hierarchy_path(sheet.name) + net_usage = self.net_usage.get(net_name, {}) + is_used = False + for usage_sheet in net_usage: + if any(p == usage_sheet for p in sheet_path): + is_used = True + break + if is_used: + needed_nets.append(self.legalize_name(net_name)) - # Only add GND if sheet actually uses it - if 'GND' in self.net_usage and sheet.name in self.net_usage['GND']: - used_nets.append('gnd') - - code.append(f"{self.tab}{func_name}({', '.join(used_nets)})\n") + code.append(f"{self.tab}{func_name}({', '.join(needed_nets)})\n") else: # Empty sheet with no components or children code.append(f"{self.tab}{func_name}()\n") @@ -460,6 +448,7 @@ def create_main_file(self, output_dir: str): main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) + def get_hierarchical_order(self): """Return sheets in dependency order.""" ordered = [] From 2e28bcb03efc6dbb15a664e10218afcf208ecb69 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 10:26:41 -0800 Subject: [PATCH 42/73] generating nets properly --- src/skidl/netlist_to_skidl.py | 68 ++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index f209ff445..1a0b3fe00 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -38,13 +38,18 @@ def extract_sheet_info(self): """Build sheet hierarchy from netlist.""" active_logger.info("=== Extracting Sheet Info ===") + # Create name-to-path mapping for sheet lookup + self.sheet_name_to_path = {} + # First pass: Create all sheets for sheet in self.netlist.sheets: - path = sheet.name.strip('/') - name = path.split('/')[-1] if path else 'main' - parent = '/'.join(path.split('/')[:-1]) if '/' in path else None + # Keep original name for use as key but clean for file/module names + original_name = sheet.name.strip('/') + path = original_name + name = original_name.split('/')[-1] if original_name else 'main' + parent = '/'.join(original_name.split('/')[:-1]) if '/' in original_name else None - self.sheets[path] = Sheet( + self.sheets[original_name] = Sheet( number=sheet.number, name=name, path=path, @@ -54,6 +59,9 @@ def extract_sheet_info(self): parent=parent, children=[] ) + + # Store mapping from sheet name to full path + self.sheet_name_to_path[name] = original_name # Second pass: Build parent-child relationships for sheet in self.sheets.values(): @@ -61,24 +69,61 @@ def extract_sheet_info(self): parent_sheet = self.sheets.get(sheet.parent) if parent_sheet: parent_sheet.children.append(sheet.path) + def get_sheet_path(self, comp): """Get sheet path from component properties.""" if isinstance(comp.properties, dict): return comp.properties.get('Sheetname', '') sheet_prop = next((p for p in comp.properties if p.name == 'Sheetname'), None) - return sheet_prop.value if sheet_prop else '' + # Preserve exact sheet name with spaces + return sheet_prop.value.strip() if sheet_prop else '' + def assign_components_to_sheets(self): """Assign components to their respective sheets.""" active_logger.info("=== Assigning Components to Sheets ===") + unassigned_components = [] + sheet_not_found = set() + for comp in self.netlist.parts: sheet_name = self.get_sheet_path(comp) if sheet_name: - for sheet in self.sheets.values(): - if sheet.name == sheet_name: + # Try to find sheet by name + sheet_found = False + for path, sheet in self.sheets.items(): + # Match using original sheet name from properties + if sheet_name == sheet.name: sheet.components.append(comp) + sheet_found = True break + + if not sheet_found: + sheet_not_found.add(sheet_name) + unassigned_components.append(comp) + else: + unassigned_components.append(comp) + + # Log warnings for unassigned components and missing sheets + if sheet_not_found: + active_logger.warning(f"Sheets not found in netlist: {sorted(sheet_not_found)}") + active_logger.warning("Components in these sheets will be assigned to root level") + + if unassigned_components: + active_logger.warning(f"Found {len(unassigned_components)} unassigned components") + # Assign unassigned components to root sheet + root = self.sheets.get('', None) + if not root: + root = Sheet( + number='0', + name='main', + path='', + components=[], + local_nets=set(), + imported_nets=set() + ) + self.sheets[''] = root + root.components.extend(unassigned_components) def get_sheet_hierarchy_path(self, sheet_name): """Get the full hierarchical path from root to given sheet.""" @@ -167,8 +212,13 @@ def analyze_nets(self): if comp.ref == pin.ref: sheet_name = self.get_sheet_path(comp) if sheet_name: - net_usage[net.name][sheet_name].add(f"{comp.ref}.{pin.num}") - active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + # Convert sheet name to full path + sheet_path = self.sheet_name_to_path.get(sheet_name) + if sheet_path: + net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") + active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + else: + active_logger.warning(f"Sheet not found for component {comp.ref}: {sheet_name}") active_logger.info("2. Analyzing Net Origins and Hierarchy:") # Second pass: Determine net origins and build hierarchy From 413cc01c1732501c8d435a79e40610c14616031e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 10:47:15 -0800 Subject: [PATCH 43/73] improving algo --- src/skidl/netlist_to_skidl.py | 187 ++++++++++++++++++++-------------- 1 file changed, 113 insertions(+), 74 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 1a0b3fe00..f73c22ec2 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -151,49 +151,6 @@ def find_lowest_common_ancestor(self, sheet1, sheet2): return common_path[-1] if common_path else None - def find_optimal_net_origin(self, used_sheets, net_name): - """ - Determine the optimal sheet to create a net based on hierarchy. - Returns (origin_sheet, paths_to_children) where: - - origin_sheet is the sheet where the net should be defined - - paths_to_children are the paths for passing the net to child sheets - """ - # Single sheet usage - create in that sheet - if len(used_sheets) == 1: - return list(used_sheets)[0], [] - - # Get sheet paths for all usage points - sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) - for sheet in used_sheets} - - # Find the common ancestors among all paths - common_ancestors = None - for paths in sheet_paths.values(): - if common_ancestors is None: - common_ancestors = set(paths) - else: - common_ancestors &= set(paths) - - if not common_ancestors: - # If no common ancestors, place at root level - return '', [] - - # Find highest common parent that contains all usage points - highest_common = min(common_ancestors, - key=lambda x: min(path.index(x) if x in path else float('inf') - for path in sheet_paths.values())) - - # Build paths from parent to children - paths = [] - for sheet in used_sheets: - if sheet != highest_common: - path = sheet_paths[sheet] - start_idx = path.index(highest_common) - child_path = path[start_idx:] - if len(child_path) > 1: # Only add if there's a path to traverse - paths.append(child_path) - - return highest_common, paths def analyze_nets(self): """Analyze nets to determine hierarchy and relationships between sheets.""" @@ -350,6 +307,64 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" + + def find_optimal_net_origin(self, used_sheets, net_name): + """ + Determine the optimal sheet to create a net based on hierarchy. + Returns (origin_sheet, paths_to_children) where: + - origin_sheet is the sheet where the net should be defined + - paths_to_children are the paths for passing the net to child sheets + """ + # If net is only used in one sheet, create it there + if len(used_sheets) == 1: + return list(used_sheets)[0], [] + + # Get sheet paths for all usage points + sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) + for sheet in used_sheets} + + # Find the lowest common ancestor that is also in the used_sheets + # This ensures we create the net in a sheet that actually uses it + common_path = None + for path in sheet_paths.values(): + if common_path is None: + common_path = path + else: + # Find common prefix + new_common = [] + for s1, s2 in zip(common_path, path): + if s1 == s2: + new_common.append(s1) + else: + break + common_path = new_common + + # Get the shallowest sheet in common_path that's in used_sheets + origin_sheet = None + for sheet in common_path: + if sheet in used_sheets: + origin_sheet = sheet + break + + if not origin_sheet: + # If no common ancestor uses the net, use the shallowest common ancestor + origin_sheet = common_path[-1] if common_path else '' + + # Build paths from origin to children + paths = [] + for sheet in used_sheets: + if sheet != origin_sheet: + path = sheet_paths[sheet] + try: + start_idx = path.index(origin_sheet) + child_path = path[start_idx:] + if len(child_path) > 1: # Only add if there's a path to traverse + paths.append(child_path) + except ValueError: + continue + + return origin_sheet, paths + def generate_sheet_code(self, sheet: Sheet) -> str: """Generate SKiDL code for a sheet.""" code = [ @@ -365,56 +380,81 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append("\n@subcircuit\n") - # Function parameters - ONLY include imported nets - params = [] - if sheet.imported_nets: - for net in sorted(sheet.imported_nets): - if not net.startswith('unconnected'): - params.append(self.legalize_name(net)) + # Function parameters - only include nets that this sheet receives from a parent + parent_provided_nets = set() + for net_name, hierarchy in self.net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + + # If this sheet uses the net but doesn't create it + if (sheet.path in hierarchy['used_in_sheets'] and + hierarchy['origin_sheet'] != sheet.path): + parent_provided_nets.add(net_name) + + # If this sheet needs to pass the net to children + for path in hierarchy['path_to_children']: + if sheet.path in path[1:]: # Skip the origin sheet + parent_provided_nets.add(net_name) + + # Sort and legalize parameter names + params = [self.legalize_name(net) for net in sorted(parent_provided_nets)] func_name = self.legalize_name(sheet.name) code.append(f"def {func_name}({', '.join(params)}):\n") - # If there are no components, children, or nets, just add return + # If no content, just return if not (sheet.components or sheet.children or sheet.local_nets): code.append(f"{self.tab}return\n") return "".join(code) - # Components + # Local nets - nets that this sheet creates + local_nets = [] + for net_name, hierarchy in self.net_hierarchy.items(): + if (not net_name.startswith('unconnected') and + hierarchy['origin_sheet'] == sheet.path): + local_nets.append(net_name) + + if local_nets: + code.append(f"{self.tab}# Local nets\n") + for net in sorted(local_nets): + legal_name = self.legalize_name(net) + code.append(f"{self.tab}{legal_name} = Net('{net}')\n") + code.append("\n") + + # Components section remains unchanged if sheet.components: code.append(f"{self.tab}# Components\n") for comp in sorted(sheet.components, key=lambda x: x.ref): code.append(self.component_to_skidl(comp)) code.append("\n") - - # Local nets - local_nets = sorted(net for net in sheet.local_nets - if not net.startswith('unconnected')) - if local_nets: - code.append(f"{self.tab}# Local nets\n") - for net in local_nets: - original_name = net - legal_name = self.legalize_name(original_name) - code.append(f"{self.tab}{legal_name} = Net('{original_name}')\n") - code.append("\n") - - # Hierarchical subcircuits + + # Call child subcircuits with appropriate nets if sheet.children: code.append(f"{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: child_sheet = self.sheets[child_path] child_func_name = self.legalize_name(child_sheet.name) - child_params = [] - - # Only pass imported nets that the child sheet actually uses - for net in sorted(child_sheet.imported_nets): - if not net.startswith('unconnected'): - if child_sheet.name in self.net_usage.get(net, {}): - child_params.append(self.legalize_name(net)) - code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") + # Collect nets needed by this child + child_nets = [] + for net_name, hierarchy in self.net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + + # Include nets used by child that come from this sheet or a parent + if child_sheet.path in hierarchy['used_in_sheets']: + child_nets.append(net_name) + + # Include nets that need to be passed through to grandchildren + for path in hierarchy['path_to_children']: + if child_sheet.path in path: + child_nets.append(net_name) - # Connections + # Legalize net names and remove duplicates + child_params = [self.legalize_name(net) for net in sorted(set(child_nets))] + code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") + + # Connections section remains unchanged if sheet.components: code.append(f"\n{self.tab}# Connections\n") for net in self.netlist.nets: @@ -422,7 +462,6 @@ def generate_sheet_code(self, sheet: Sheet) -> str: if conn: code.append(conn) - # Always add return statement at the end code.append(f"{self.tab}return\n") return "".join(code) From 110673bfac87d310b425a47062179b47032f0e7e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 10:53:55 -0800 Subject: [PATCH 44/73] almost there --- src/skidl/netlist_to_skidl.py | 80 +++++++++++++++++------------------ 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index f73c22ec2..9ebc632d3 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -307,7 +307,6 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" - def find_optimal_net_origin(self, used_sheets, net_name): """ Determine the optimal sheet to create a net based on hierarchy. @@ -324,7 +323,6 @@ def find_optimal_net_origin(self, used_sheets, net_name): for sheet in used_sheets} # Find the lowest common ancestor that is also in the used_sheets - # This ensures we create the net in a sheet that actually uses it common_path = None for path in sheet_paths.values(): if common_path is None: @@ -339,18 +337,10 @@ def find_optimal_net_origin(self, used_sheets, net_name): break common_path = new_common - # Get the shallowest sheet in common_path that's in used_sheets - origin_sheet = None - for sheet in common_path: - if sheet in used_sheets: - origin_sheet = sheet - break - - if not origin_sheet: - # If no common ancestor uses the net, use the shallowest common ancestor - origin_sheet = common_path[-1] if common_path else '' + # Get the shallowest sheet that actually uses this net + origin_sheet = common_path[-1] if common_path else '' - # Build paths from origin to children + # Build paths from origin to children that use this net paths = [] for sheet in used_sheets: if sheet != origin_sheet: @@ -380,34 +370,37 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append("\n@subcircuit\n") - # Function parameters - only include nets that this sheet receives from a parent - parent_provided_nets = set() + # Function parameters - only include nets that are: + # 1. Used in this sheet but created in a parent OR + # 2. Created in this sheet and used by children + required_nets = set() + for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue + + origin = hierarchy['origin_sheet'] + used_in = hierarchy['used_in_sheets'] + paths = hierarchy['path_to_children'] - # If this sheet uses the net but doesn't create it - if (sheet.path in hierarchy['used_in_sheets'] and - hierarchy['origin_sheet'] != sheet.path): - parent_provided_nets.add(net_name) + # If net is used here but created in parent + if sheet.path in used_in and origin != sheet.path: + required_nets.add(net_name) - # If this sheet needs to pass the net to children - for path in hierarchy['path_to_children']: - if sheet.path in path[1:]: # Skip the origin sheet - parent_provided_nets.add(net_name) + # If net is created here and used by children + elif origin == sheet.path and any(sheet.path in path for path in paths): + for path in paths: + if sheet.path in path: + required_nets.add(net_name) + break # Sort and legalize parameter names - params = [self.legalize_name(net) for net in sorted(parent_provided_nets)] + params = [self.legalize_name(net) for net in sorted(required_nets)] func_name = self.legalize_name(sheet.name) code.append(f"def {func_name}({', '.join(params)}):\n") - # If no content, just return - if not (sheet.components or sheet.children or sheet.local_nets): - code.append(f"{self.tab}return\n") - return "".join(code) - - # Local nets - nets that this sheet creates + # Local nets - only nets that are created in this sheet local_nets = [] for net_name, hierarchy in self.net_hierarchy.items(): if (not net_name.startswith('unconnected') and @@ -421,40 +414,45 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append(f"{self.tab}{legal_name} = Net('{net}')\n") code.append("\n") - # Components section remains unchanged + # Components section if sheet.components: code.append(f"{self.tab}# Components\n") for comp in sorted(sheet.components, key=lambda x: x.ref): code.append(self.component_to_skidl(comp)) code.append("\n") - - # Call child subcircuits with appropriate nets + + # Call child subcircuits if sheet.children: code.append(f"{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: child_sheet = self.sheets[child_path] child_func_name = self.legalize_name(child_sheet.name) - # Collect nets needed by this child + # Only pass nets that the child needs from this sheet or above child_nets = [] for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - # Include nets used by child that come from this sheet or a parent + # Include net if: + # 1. Child uses it AND it comes from this sheet or above + # 2. Net needs to be passed through to grandchildren + origin = hierarchy['origin_sheet'] if child_sheet.path in hierarchy['used_in_sheets']: - child_nets.append(net_name) - - # Include nets that need to be passed through to grandchildren + sheet_path = self.get_sheet_hierarchy_path(sheet.path) + if origin in sheet_path: + child_nets.append(net_name) + + # Check if needed for grandchildren for path in hierarchy['path_to_children']: - if child_sheet.path in path: + if child_sheet.path in path and sheet.path == origin: child_nets.append(net_name) # Legalize net names and remove duplicates child_params = [self.legalize_name(net) for net in sorted(set(child_nets))] code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") - - # Connections section remains unchanged + + # Connections if sheet.components: code.append(f"\n{self.tab}# Connections\n") for net in self.netlist.nets: From 16373bc7fa9d37dd101fddc33ade1facf46df2b5 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 11:32:55 -0800 Subject: [PATCH 45/73] logic somewhat working on some projects --- src/skidl/netlist_to_skidl.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 9ebc632d3..02e3fcfa5 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -361,41 +361,34 @@ def generate_sheet_code(self, sheet: Sheet) -> str: "# -*- coding: utf-8 -*-\n", "from skidl import *\n" ] - - # Import child subcircuits + + # Import child subcircuits for child_path in sheet.children: child_sheet = self.sheets[child_path] module_name = self.legalize_name(child_sheet.name) code.append(f"from {module_name} import {module_name}\n") - + code.append("\n@subcircuit\n") - - # Function parameters - only include nets that are: - # 1. Used in this sheet but created in a parent OR - # 2. Created in this sheet and used by children + + # Function parameters - only include nets that are used in this sheet + # but created in an ancestor sheet required_nets = set() - for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue origin = hierarchy['origin_sheet'] used_in = hierarchy['used_in_sheets'] - paths = hierarchy['path_to_children'] - - # If net is used here but created in parent - if sheet.path in used_in and origin != sheet.path: - required_nets.add(net_name) - - # If net is created here and used by children - elif origin == sheet.path and any(sheet.path in path for path in paths): - for path in paths: - if sheet.path in path: + + if sheet.path in used_in: # Net is used here + if origin != sheet.path: # Net is created somewhere else + # Check if origin is an ancestor + sheet_path = self.get_sheet_hierarchy_path(sheet.path) + if origin in sheet_path: required_nets.add(net_name) - break # Sort and legalize parameter names - params = [self.legalize_name(net) for net in sorted(required_nets)] + params = [self.legalize_name(net) for net in sorted(required_nets)] func_name = self.legalize_name(sheet.name) code.append(f"def {func_name}({', '.join(params)}):\n") From 2b75c0b9c03db545416bf23ddab773bd7c571a59 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 12:37:53 -0800 Subject: [PATCH 46/73] code is close to working --- query_gen.py | 184 ++++++++++++ src/skidl/netlist_to_skidl.py | 523 +++++++++++++++++----------------- 2 files changed, 442 insertions(+), 265 deletions(-) create mode 100644 query_gen.py diff --git a/query_gen.py b/query_gen.py new file mode 100644 index 000000000..844d731de --- /dev/null +++ b/query_gen.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 + +#============================================================================== +# QUICK EDIT CONFIGURATION - Modify these values as needed +#============================================================================== + +# Where to look for files +ROOT_DIRECTORY = "/Users/shanemattner/Desktop/skidl" + +# Where to save the combined output +OUTPUT_FILE = "collected_code.txt" + +# What files to collect (add or remove filenames as needed) +TARGET_FILES = [ + 'netlist_to_skidl.py', + 'main_control_board.py', + 'Block_Diagram.py', + 'Project_Architecture.py', + 'Power_Info.py', + 'Revision_History.py', + 'Coral_TPU.py', + 'ESP32S3.PY', + 'Left_Leg.py', + 'Right_Leg.py', + 'Voltage_Regulators.py', + 'main_simple_project.py', + 'esp32s3mini1.py', + '_3v3_regulator.py', + 'resistor_divider1.py', + 'test_examples.py', + 'USB.py', +] + +# Message to add at the start of the output file +INTRO_MESSAGE = """ + +netlist_to_skidl.py is the source file for logic that converts a KiCad netlist to a SKiDL script. + +This logic seems to work well for a complex KiCAD project. See the files: + 'main_control_board.py', + 'Block_Diagram.py', + 'Project_Architecture.py', + 'Power_Info.py', + 'Revision_History.py', + 'Coral_TPU.py', + 'ESP32S3.PY', + 'Left_Leg.py', + 'Right_Leg.py', + 'Voltage_Regulators.py', + +However, this logic breaks when trying to convert a KiCAD netlist for a simple project. See the files: + + 'main_simple_project.py', + 'esp32s3mini1.py', + '_3v3_regulator.py', + 'resistor_divider1.py', + 'test_examples.py', + 'USB.py', + +Analyze netlist_to_skidl.py to determine why it breaks for simple projects. The goal is to make it work for both simple and complex projects. +The algorithm should be general enough that it works for any KiCAD project. The algorithm should not be hard-coded to work for specific projects. + +Do not produce any code yet. Just analyze the existing code and determine what changes are needed to make it work for both simple and complex projects. +Ask me questions to clarify anything you do not fully understand. Give me examples of what you are thinking. + +""" + +#============================================================================== +# Script Implementation - No need to modify below this line +#============================================================================== + +""" +File Collector for Query Building + +This script combines specific files into a single output file to help build +queries when iterating on software development. Edit the CONFIGURATION section +at the top to customize which files to collect. +""" + +import os +from typing import List +from dataclasses import dataclass + +@dataclass +class FileCollectorConfig: + """Configuration class to store all script parameters""" + root_directory: str + output_filename: str + target_filenames: List[str] + intro_message: str + +def create_config_from_settings() -> FileCollectorConfig: + """Creates configuration object from the settings defined at the top of the script""" + return FileCollectorConfig( + root_directory=ROOT_DIRECTORY, + output_filename=OUTPUT_FILE, + target_filenames=TARGET_FILES, + intro_message=INTRO_MESSAGE + ) + +def is_target_file(filename: str, target_files: List[str]) -> bool: + """ + Check if a filename matches one of our target filenames. + + Args: + filename: Name of the file to check + target_files: List of target filenames to match against + """ + return os.path.basename(filename) in target_files + +def find_target_files(config: FileCollectorConfig) -> List[str]: + """ + Search for target files in the root directory. + + Args: + config: Configuration object containing search parameters + + Returns: + List[str]: List of full file paths for matching files + """ + collected_files = [] + + # Walk through the directory tree + for dirpath, _, filenames in os.walk(config.root_directory): + for filename in filenames: + if is_target_file(filename, config.target_filenames): + full_path = os.path.join(dirpath, filename) + if os.path.isfile(full_path): + collected_files.append(full_path) + + return sorted(collected_files) + +def write_combined_file(collected_files: List[str], config: FileCollectorConfig) -> None: + """ + Write all collected file contents to a single output file. + + Args: + collected_files: List of file paths to combine + config: Configuration object containing output settings + """ + with open(config.output_filename, 'w') as out_file: + # Write the introduction message + out_file.write(config.intro_message + "\n") + + # Process each collected file + total_lines = 0 + for file_path in collected_files: + try: + # Read and write each file's contents with clear separation + with open(file_path, 'r') as input_file: + content = input_file.read() + filename = os.path.basename(file_path) + + # Add clear separators around file content + out_file.write(f"\n/* Begin of file: {filename} */\n") + out_file.write(content) + out_file.write(f"\n/* End of file: {filename} */\n") + + # Print statistics for monitoring + num_lines = len(content.splitlines()) + total_lines += num_lines + print(f"{filename}: {num_lines} lines") + + except Exception as e: + print(f"Error processing {file_path}: {e}") + print(f"Total lines written: {total_lines}") + +def main(): + """Main execution function""" + # Create configuration from settings + config = create_config_from_settings() + + # Find all matching files + collected_files = find_target_files(config) + + # Combine files into output + write_combined_file(collected_files, config) + + # Print summary + print(f"\nProcessed {len(collected_files)} files") + print(f"Output saved to: {config.output_filename}") + +if __name__ == "__main__": + main() diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 02e3fcfa5..2985978cb 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -34,42 +34,6 @@ def __init__(self, netlist_src): self.sheets = {} self.tab = " " * 4 - def extract_sheet_info(self): - """Build sheet hierarchy from netlist.""" - active_logger.info("=== Extracting Sheet Info ===") - - # Create name-to-path mapping for sheet lookup - self.sheet_name_to_path = {} - - # First pass: Create all sheets - for sheet in self.netlist.sheets: - # Keep original name for use as key but clean for file/module names - original_name = sheet.name.strip('/') - path = original_name - name = original_name.split('/')[-1] if original_name else 'main' - parent = '/'.join(original_name.split('/')[:-1]) if '/' in original_name else None - - self.sheets[original_name] = Sheet( - number=sheet.number, - name=name, - path=path, - components=[], - local_nets=set(), - imported_nets=set(), - parent=parent, - children=[] - ) - - # Store mapping from sheet name to full path - self.sheet_name_to_path[name] = original_name - - # Second pass: Build parent-child relationships - for sheet in self.sheets.values(): - if sheet.parent: - parent_sheet = self.sheets.get(sheet.parent) - if parent_sheet: - parent_sheet.children.append(sheet.path) - def get_sheet_path(self, comp): """Get sheet path from component properties.""" @@ -79,52 +43,6 @@ def get_sheet_path(self, comp): # Preserve exact sheet name with spaces return sheet_prop.value.strip() if sheet_prop else '' - - def assign_components_to_sheets(self): - """Assign components to their respective sheets.""" - active_logger.info("=== Assigning Components to Sheets ===") - unassigned_components = [] - sheet_not_found = set() - - for comp in self.netlist.parts: - sheet_name = self.get_sheet_path(comp) - if sheet_name: - # Try to find sheet by name - sheet_found = False - for path, sheet in self.sheets.items(): - # Match using original sheet name from properties - if sheet_name == sheet.name: - sheet.components.append(comp) - sheet_found = True - break - - if not sheet_found: - sheet_not_found.add(sheet_name) - unassigned_components.append(comp) - else: - unassigned_components.append(comp) - - # Log warnings for unassigned components and missing sheets - if sheet_not_found: - active_logger.warning(f"Sheets not found in netlist: {sorted(sheet_not_found)}") - active_logger.warning("Components in these sheets will be assigned to root level") - - if unassigned_components: - active_logger.warning(f"Found {len(unassigned_components)} unassigned components") - # Assign unassigned components to root sheet - root = self.sheets.get('', None) - if not root: - root = Sheet( - number='0', - name='main', - path='', - components=[], - local_nets=set(), - imported_nets=set() - ) - self.sheets[''] = root - root.components.extend(unassigned_components) - def get_sheet_hierarchy_path(self, sheet_name): """Get the full hierarchical path from root to given sheet.""" path = [] @@ -152,82 +70,6 @@ def find_lowest_common_ancestor(self, sheet1, sheet2): return common_path[-1] if common_path else None - def analyze_nets(self): - """Analyze nets to determine hierarchy and relationships between sheets.""" - active_logger.info("=== Starting Net Analysis ===") - - # Data structures for analysis - net_usage = defaultdict(lambda: defaultdict(set)) # net -> sheet -> pins - net_hierarchy = {} # net -> {origin_sheet, used_in_sheets, path_to_children} - - active_logger.info("1. Mapping Net Usage Across Sheets:") - # First pass: Build net usage map - for net in self.netlist.nets: - active_logger.debug(f"\nAnalyzing net: {net.name}") - for pin in net.pins: - for comp in self.netlist.parts: - if comp.ref == pin.ref: - sheet_name = self.get_sheet_path(comp) - if sheet_name: - # Convert sheet name to full path - sheet_path = self.sheet_name_to_path.get(sheet_name) - if sheet_path: - net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") - active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") - else: - active_logger.warning(f"Sheet not found for component {comp.ref}: {sheet_name}") - - active_logger.info("2. Analyzing Net Origins and Hierarchy:") - # Second pass: Determine net origins and build hierarchy - for net_name, sheet_usages in net_usage.items(): - active_logger.debug(f"\nNet: {net_name}") - used_sheets = set(sheet_usages.keys()) - - # Find optimal origin and paths - origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) - - net_hierarchy[net_name] = { - 'origin_sheet': origin_sheet, - 'used_in_sheets': used_sheets, - 'path_to_children': paths_to_children - } - - active_logger.debug(f" - Origin sheet: {origin_sheet}") - active_logger.debug(f" - Used in sheets: {used_sheets}") - active_logger.debug(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") - - # Third pass: Update sheet net classifications - for sheet in self.sheets.values(): - sheet.local_nets.clear() - sheet.imported_nets.clear() - - active_logger.debug(f"\nSheet: {sheet.name}") - for net_name, hierarchy in net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - - # If this sheet is the origin, it's a local net - if hierarchy['origin_sheet'] == sheet.name: - sheet.local_nets.add(net_name) - active_logger.debug(f" - Local net: {net_name}") - - # If used in this sheet but originates elsewhere, it's imported - elif sheet.name in hierarchy['used_in_sheets']: - sheet.imported_nets.add(net_name) - active_logger.debug(f" - Imported net: {net_name}") - - # If this sheet is in the path between origin and users, it needs to pass the net - else: - for path in hierarchy['path_to_children']: - if sheet.name in path: - sheet.imported_nets.add(net_name) - active_logger.debug(f" - Imported net (path): {net_name}") - break - - # Store the analysis results for use in code generation - self.net_hierarchy = net_hierarchy - self.net_usage = net_usage - def legalize_name(self, name: str, is_filename: bool = False) -> str: """Convert any name into a valid Python identifier.""" # Remove leading slashes and spaces @@ -355,97 +197,319 @@ def find_optimal_net_origin(self, used_sheets, net_name): return origin_sheet, paths + def extract_sheet_info(self): + """Build sheet hierarchy from netlist.""" + active_logger.info("=== Extracting Sheet Info ===") + + # Create name-to-path mapping for sheet lookup. + self.sheet_name_to_path = {} + + # First pass: Create all sheets. + for sheet in self.netlist.sheets: + # Strip leading/trailing slashes. + original_name = sheet.name.strip('/') + # If empty, then use "main". + if not original_name: + original_name = "main" + # Use the last element as the local name. + name = original_name.split('/')[-1] + # Determine the parent based on hierarchy. + # If there is a '/' in the original name, then parent is everything before the last slash. + # Otherwise, if this sheet is not "main", force its parent to be "main". + if '/' in original_name: + parent = '/'.join(original_name.split('/')[:-1]) + # If parent becomes empty, set to main. + if not parent: + parent = "main" + else: + parent = "main" if name != "main" else None + + self.sheets[original_name] = Sheet( + number=sheet.number, + name=name, + path=original_name, + components=[], + local_nets=set(), + imported_nets=set(), + parent=parent, + children=[] + ) + + # Store mapping from local name to full hierarchical path. + self.sheet_name_to_path[name] = original_name + + # Second pass: Build parent-child relationships. + for sheet in self.sheets.values(): + if sheet.parent: + parent_sheet = self.sheets.get(sheet.parent) + if parent_sheet: + parent_sheet.children.append(sheet.path) + else: + # In case the parent sheet doesn't exist in our mapping, we create a root. + root = self.sheets.get("main") + if not root: + root = Sheet( + number='0', + name='main', + path='main', + components=[], + local_nets=set(), + imported_nets=set() + ) + self.sheets["main"] = root + sheet.parent = "main" + root.children.append(sheet.path) + + def assign_components_to_sheets(self): + """Assign components to their respective sheets.""" + active_logger.info("=== Assigning Components to Sheets ===") + unassigned_components = [] + sheet_not_found = set() + + for comp in self.netlist.parts: + sheet_name = self.get_sheet_path(comp) + if sheet_name: + sheet_found = False + # Try to match against the sheet's local name + for path, sheet in self.sheets.items(): + if sheet_name == sheet.name: + sheet.components.append(comp) + sheet_found = True + break + if not sheet_found: + sheet_not_found.add(sheet_name) + unassigned_components.append(comp) + else: + unassigned_components.append(comp) + + if sheet_not_found: + active_logger.warning(f"Sheets not found in netlist: {sorted(sheet_not_found)}") + active_logger.warning("Components in these sheets will be assigned to root level") + + if unassigned_components: + active_logger.warning(f"Found {len(unassigned_components)} unassigned components") + root = self.sheets.get("main", None) + if not root: + root = Sheet( + number='0', + name='main', + path='main', + components=[], + local_nets=set(), + imported_nets=set() + ) + self.sheets["main"] = root + root.components.extend(unassigned_components) + + def analyze_nets(self): + """Analyze nets to determine hierarchy and relationships between sheets.""" + active_logger.info("=== Starting Net Analysis ===") + + # Map net usage: net_name -> { sheet_path: set(pins) } + net_usage = defaultdict(lambda: defaultdict(set)) + + active_logger.info("1. Mapping Net Usage Across Sheets:") + for net in self.netlist.nets: + active_logger.debug(f"\nAnalyzing net: {net.name}") + for pin in net.pins: + for comp in self.netlist.parts: + if comp.ref == pin.ref: + sheet_name = self.get_sheet_path(comp) + if sheet_name: + sheet_path = self.sheet_name_to_path.get(sheet_name) + if sheet_path: + net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") + active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + else: + active_logger.warning(f"Sheet not found for component {comp.ref}: {sheet_name}") + + active_logger.info("2. Analyzing Net Origins and Hierarchy:") + net_hierarchy = {} + for net_name, sheet_usages in net_usage.items(): + active_logger.debug(f"\nNet: {net_name}") + used_sheets = set(sheet_usages.keys()) + + # Determine optimal origin by finding the lowest common ancestor among all sheets that use this net. + origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) + + net_hierarchy[net_name] = { + 'origin_sheet': origin_sheet, + 'used_in_sheets': used_sheets, + 'path_to_children': paths_to_children + } + + active_logger.debug(f" - Origin sheet: {origin_sheet}") + active_logger.debug(f" - Used in sheets: {used_sheets}") + active_logger.debug(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") + + # Third pass: Classify nets for each sheet. + for sheet in self.sheets.values(): + sheet.local_nets.clear() + sheet.imported_nets.clear() + + active_logger.debug(f"\nSheet: {sheet.name}") + for net_name, hierarchy in net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + + # If this sheet is the origin, mark net as local. + if hierarchy['origin_sheet'] == sheet.path: + sheet.local_nets.add(net_name) + active_logger.debug(f" - Local net: {net_name}") + # If this sheet is among those that use the net but not the origin, mark it as imported. + elif sheet.path in hierarchy['used_in_sheets']: + sheet.imported_nets.add(net_name) + active_logger.debug(f" - Imported net: {net_name}") + else: + for path in hierarchy['path_to_children']: + if sheet.path in path: + sheet.imported_nets.add(net_name) + active_logger.debug(f" - Imported net (path): {net_name}") + break + + self.net_hierarchy = net_hierarchy + self.net_usage = net_usage + + def create_main_file(self, output_dir: str): + """Create the main.py file.""" + code = [ + "# -*- coding: utf-8 -*-\n", + "from skidl import *\n" + ] + + # Import only top-level modules (those with no parent except main itself). + for sheet in self.sheets.values(): + if (not sheet.parent) and sheet.name != 'main': + module_name = self.legalize_name(sheet.name) + code.append(f"from {module_name} import {module_name}\n") + + code.extend([ + "\ndef main():\n", + ]) + + # Build a set of top-level nets. + top_level_nets = set() + for net_name, hierarchy in self.net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + # If the net originates in "main" or is used across multiple top-level sheets, + # mark it as global. + if (hierarchy['origin_sheet'] == "main" or + len([s for s in hierarchy['used_in_sheets'] if not self.sheets[s].parent]) > 1): + top_level_nets.add(net_name) + + if top_level_nets: + code.append(f"{self.tab}# Create global nets\n") + for net_name in sorted(top_level_nets): + legal_name = self.legalize_name(net_name) + code.append(f"{self.tab}{legal_name} = Net('{net_name}')\n") + code.append("\n") + + # Call top-level subcircuits. + code.append(f"{self.tab}# Create subcircuits\n") + for sheet in self.get_hierarchical_order(): + # Only call subcircuits that are top-level (or have no parent) and are not "main" itself. + if not sheet.parent and sheet.name != 'main': + func_name = self.legalize_name(sheet.name) + needed_nets = [] + for net_name in sorted(sheet.imported_nets): + if not net_name.startswith('unconnected'): + # Verify that the net is indeed used in the sheet or its children. + sheet_path = self.get_sheet_hierarchy_path(sheet.path) + net_usage_sheets = self.net_usage.get(net_name, {}) + if any(usage_sheet in sheet_path for usage_sheet in net_usage_sheets): + needed_nets.append(self.legalize_name(net_name)) + code.append(f"{self.tab}{func_name}({', '.join(needed_nets)})\n") + else: + code.append(f"{self.tab}{self.legalize_name(sheet.name)}()\n") + + code.extend([ + "\nif __name__ == \"__main__\":\n", + f"{self.tab}main()\n", + f"{self.tab}generate_netlist()\n" + ]) + + main_path = Path(output_dir) / "main.py" + main_path.write_text("".join(code)) + def generate_sheet_code(self, sheet: Sheet) -> str: """Generate SKiDL code for a sheet.""" code = [ "# -*- coding: utf-8 -*-\n", "from skidl import *\n" ] - - # Import child subcircuits + + # Import child subcircuits. for child_path in sheet.children: child_sheet = self.sheets[child_path] module_name = self.legalize_name(child_sheet.name) code.append(f"from {module_name} import {module_name}\n") - + code.append("\n@subcircuit\n") - - # Function parameters - only include nets that are used in this sheet - # but created in an ancestor sheet + + # Determine required nets: these are nets used in this sheet but not created locally. required_nets = set() for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - origin = hierarchy['origin_sheet'] - used_in = hierarchy['used_in_sheets'] - - if sheet.path in used_in: # Net is used here - if origin != sheet.path: # Net is created somewhere else - # Check if origin is an ancestor - sheet_path = self.get_sheet_hierarchy_path(sheet.path) - if origin in sheet_path: + if sheet.path in hierarchy['used_in_sheets'] and origin != sheet.path: + required_nets.add(net_name) + else: + # Also check if any path from the origin to a child of this sheet passes through this sheet. + for path in hierarchy['path_to_children']: + if sheet.path in path: required_nets.add(net_name) - - # Sort and legalize parameter names + break + + # Legalize and sort parameter names. params = [self.legalize_name(net) for net in sorted(required_nets)] - func_name = self.legalize_name(sheet.name) code.append(f"def {func_name}({', '.join(params)}):\n") - # Local nets - only nets that are created in this sheet + # Create local nets: nets for which this sheet is the origin. local_nets = [] for net_name, hierarchy in self.net_hierarchy.items(): if (not net_name.startswith('unconnected') and hierarchy['origin_sheet'] == sheet.path): local_nets.append(net_name) - + if local_nets: code.append(f"{self.tab}# Local nets\n") for net in sorted(local_nets): legal_name = self.legalize_name(net) code.append(f"{self.tab}{legal_name} = Net('{net}')\n") code.append("\n") - - # Components section + + # Components section. if sheet.components: code.append(f"{self.tab}# Components\n") for comp in sorted(sheet.components, key=lambda x: x.ref): code.append(self.component_to_skidl(comp)) code.append("\n") - - # Call child subcircuits + + # Call child subcircuits. if sheet.children: code.append(f"{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: child_sheet = self.sheets[child_path] child_func_name = self.legalize_name(child_sheet.name) - - # Only pass nets that the child needs from this sheet or above child_nets = [] for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - - # Include net if: - # 1. Child uses it AND it comes from this sheet or above - # 2. Net needs to be passed through to grandchildren origin = hierarchy['origin_sheet'] if child_sheet.path in hierarchy['used_in_sheets']: sheet_path = self.get_sheet_hierarchy_path(sheet.path) if origin in sheet_path: child_nets.append(net_name) - - # Check if needed for grandchildren for path in hierarchy['path_to_children']: if child_sheet.path in path and sheet.path == origin: child_nets.append(net_name) - - # Legalize net names and remove duplicates child_params = [self.legalize_name(net) for net in sorted(set(child_nets))] code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") - - # Connections + + # Connections. if sheet.components: code.append(f"\n{self.tab}# Connections\n") for net in self.netlist.nets: @@ -454,79 +518,8 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append(conn) code.append(f"{self.tab}return\n") - - return "".join(code) - - def create_main_file(self, output_dir: str): - """Create the main.py file.""" - code = [ - "# -*- coding: utf-8 -*-\n", - "from skidl import *\n" - ] - - # Import only top-level modules - for sheet in self.sheets.values(): - if not sheet.parent and sheet.name != 'main': - module_name = self.legalize_name(sheet.name) - code.append(f"from {module_name} import {module_name}\n") - - code.extend([ - "\ndef main():\n", - ]) - - # Find all nets that need to be created at the top level - top_level_nets = set() - for net_name, hierarchy in self.net_hierarchy.items(): - if not net_name.startswith('unconnected'): - # If net originates at root level or is used across multiple top-level sheets - if (not hierarchy['origin_sheet'] or - len([s for s in hierarchy['used_in_sheets'] if not self.sheets[s].parent]) > 1): - top_level_nets.add(net_name) - - # Create all top-level nets - if top_level_nets: - code.append(f"{self.tab}# Create global nets\n") - for net_name in sorted(top_level_nets): - legal_name = self.legalize_name(net_name) - code.append(f"{self.tab}{legal_name} = Net('{net_name}')\n") - code.append("\n") - - # Call only top-level subcircuits - code.append(f"{self.tab}# Create subcircuits\n") - for sheet in self.get_hierarchical_order(): - if not sheet.parent and sheet.name != 'main': - func_name = self.legalize_name(sheet.name) - - # Only process sheets that have components or children - if sheet.components or sheet.children: - # Get all nets that need to be passed to this sheet - needed_nets = [] - for net_name in sorted(sheet.imported_nets): - if not net_name.startswith('unconnected'): - # Only include nets that are actually used in this sheet or its children - sheet_path = self.get_sheet_hierarchy_path(sheet.name) - net_usage = self.net_usage.get(net_name, {}) - is_used = False - for usage_sheet in net_usage: - if any(p == usage_sheet for p in sheet_path): - is_used = True - break - if is_used: - needed_nets.append(self.legalize_name(net_name)) - - code.append(f"{self.tab}{func_name}({', '.join(needed_nets)})\n") - else: - # Empty sheet with no components or children - code.append(f"{self.tab}{func_name}()\n") - code.extend([ - "\nif __name__ == \"__main__\":\n", - f"{self.tab}main()\n", - f"{self.tab}generate_netlist()\n" - ]) - - main_path = Path(output_dir) / "main.py" - main_path.write_text("".join(code)) + return "".join(code) def get_hierarchical_order(self): From d42f337139e0a2c348caf4cb1efc93b32a737453 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 13:29:19 -0800 Subject: [PATCH 47/73] getting closer, most of the circuits are correct --- query_gen.py | 82 ++++++---- src/skidl/netlist_to_skidl.py | 298 ++++++++++------------------------ 2 files changed, 140 insertions(+), 240 deletions(-) diff --git a/query_gen.py b/query_gen.py index 844d731de..3d1daaf2b 100644 --- a/query_gen.py +++ b/query_gen.py @@ -29,42 +29,68 @@ 'resistor_divider1.py', 'test_examples.py', 'USB.py', + 'example_kicad_project.net', + 'report.txt' ] # Message to add at the start of the output file INTRO_MESSAGE = """ -netlist_to_skidl.py is the source file for logic that converts a KiCad netlist to a SKiDL script. - -This logic seems to work well for a complex KiCAD project. See the files: - 'main_control_board.py', - 'Block_Diagram.py', - 'Project_Architecture.py', - 'Power_Info.py', - 'Revision_History.py', - 'Coral_TPU.py', - 'ESP32S3.PY', - 'Left_Leg.py', - 'Right_Leg.py', - 'Voltage_Regulators.py', - -However, this logic breaks when trying to convert a KiCAD netlist for a simple project. See the files: - - 'main_simple_project.py', - 'esp32s3mini1.py', - '_3v3_regulator.py', - 'resistor_divider1.py', - 'test_examples.py', - 'USB.py', - -Analyze netlist_to_skidl.py to determine why it breaks for simple projects. The goal is to make it work for both simple and complex projects. -The algorithm should be general enough that it works for any KiCAD project. The algorithm should not be hard-coded to work for specific projects. - -Do not produce any code yet. Just analyze the existing code and determine what changes are needed to make it work for both simple and complex projects. -Ask me questions to clarify anything you do not fully understand. Give me examples of what you are thinking. +netlist_to_skidl.py is the source file for logic that converts a KiCad netlist into equivalent hierarchical SKiDL programs. + +This logic works well for a complex KiCad project. See the following files for the complex project: + - main_control_board.py + - Block_Diagram.py + - Project_Architecture.py + - Power_Info.py + - Revision_History.py + - Coral_TPU.py + - ESP32S3.PY + - Left_Leg.py + - Right_Leg.py + - Voltage_Regulators.py + +However, when converting a KiCad netlist for a simple project (see these files): + - example_kicad_project.net (netlist file being processed) + - main_simple_project.py + - esp32s3mini1.py + - _3v3_regulator.py + - resistor_divider1.py + - test_examples.py + - USB.py + - report.txt + +The logic seems to work fairly well except: +- **esp32s3mini1() Parameter Issue:** + The generated subcircuit definition for `esp32s3mini1` erroneously includes an argument named `esp32s3mini1_HW_VER`. This net is not needed as an external parameter for this subcircuit. After manually removing this argument and running the generated main file, the output netlist imports correctly into KiCad (see report.txt for import logs). The problem appears to be in the net analysis logic, where the converter mistakenly classifies the net named `/esp32s3mini1/HW_VER` as imported rather than local. The solution is to adjust the net classification so that if the net originates in the same sheet as the subcircuit (or is intended to be local), it is not passed in as a parameter. + +- **main_control_board.py Subcircuit Calls:** + In the complex project, the generated `main_control_board.py` does not call any of the subsheets. In other words, the top-level file fails to invoke the subcircuits (such as Block_Diagram, ESP32S3, Voltage_Regulators, etc.). This indicates that the logic used to decide which sheets are “top-level” (and thus should be called from main) is not detecting them properly. The net analysis and sheet‐hierarchy code must be adjusted so that the top‐level aggregator (or main sheet) correctly calls all its child subcircuits—even if some of them do not import any nets—so that the hierarchical structure is fully instantiated in the generated main file. + +- **Project_Architecture() Parameter Issue:** + The generated definition for `Project_Architecture()` (which serves as the top-level sheet for the board) erroneously includes many parameters (e.g. `_p_3_3V`, `_p_5V`, and several others). Since Project_Architecture.py is intended to be the top-level sheet (with its purpose being mainly to document the overall architecture), it should not require any externally passed parameters; it should instead define its own local nets (or rely on the global definitions created by main). This indicates that the converter’s heuristic for deciding which nets are “imported” (i.e. must be passed in as parameters) is treating nets used in Project_Architecture as imported rather than local. The solution is to modify the net classification logic so that for the top-level aggregator sheet, all nets that it uses are defined locally and no parameters are required. + +In summary, the underlying issue in both simple and complex projects is how the converter’s net analysis and sheet‐hierarchy logic decide whether a net is “imported” (and thus should appear as a function parameter) or “local” (defined inside the subcircuit). For the simple project, no parameters are passed to subcircuits (even though the design requires them), while for the complex project the top-level Project_Architecture subcircuit is erroneously given a long parameter list. The fix must be made purely through analysis (e.g. by comparing the sheet’s “origin” for a net versus its usage) rather than by hardcoding specific net or sheet names. + +**Where to Look for a Solution:** + +- **netlist_to_skidl.py:** + • In the `analyze_nets()` method, review how nets are classified as local versus imported. The logic that checks whether a net “originates” in the same sheet should be adjusted so that, for example, the `/esp32s3mini1/HW_VER` net is marked as local (thus not passed in as a parameter to `esp32s3mini1`). + • In the `create_main_file()` method, verify the conditions used to decide which sheets are “top-level” (i.e. those that should be called from main). The condition should not omit subcircuits from being called if they belong to the top-level hierarchy. + +- **Sheet Hierarchy:** + Examine how sheet parent/child relationships are built in `extract_sheet_info()` and how they influence the net analysis. Ensure that a top-level aggregator like Project_Architecture is treated as “local” for its nets, so that no external parameters are needed. + +- **Subcircuit Calls in main_simple_project.py and main_control_board.py:** + Compare the generated main files for the simple and complex projects. The simple project should have calls like + ```python + esp32s3mini1(_p_3V3, _3v3_monitor, _5v_monitor, D_p, D_n, GND) + _3v3_regulator(_p_3V3, _p_5V, _3v3_monitor, _5v_monitor, GND) + USB(_p_5V, D_p, D_n, GND) """ + #============================================================================== # Script Implementation - No need to modify below this line #============================================================================== diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 2985978cb..9d061a774 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -9,7 +9,7 @@ from pathlib import Path from collections import defaultdict from dataclasses import dataclass -from typing import Dict, List, Set +from typing import List, Set from kinparse import parse_netlist from .logger import active_logger @@ -28,23 +28,22 @@ def __post_init__(self): if self.children is None: self.children = [] + class HierarchicalConverter: def __init__(self, netlist_src): self.netlist = parse_netlist(netlist_src) self.sheets = {} self.tab = " " * 4 - def get_sheet_path(self, comp): - """Get sheet path from component properties.""" + """Return the sheet name from the component properties.""" if isinstance(comp.properties, dict): return comp.properties.get('Sheetname', '') sheet_prop = next((p for p in comp.properties if p.name == 'Sheetname'), None) - # Preserve exact sheet name with spaces return sheet_prop.value.strip() if sheet_prop else '' def get_sheet_hierarchy_path(self, sheet_name): - """Get the full hierarchical path from root to given sheet.""" + """Return a list representing the hierarchy from root to the given sheet.""" path = [] current = sheet_name while current in self.sheets: @@ -55,73 +54,47 @@ def get_sheet_hierarchy_path(self, sheet_name): return list(reversed(path)) def find_lowest_common_ancestor(self, sheet1, sheet2): - """Find the lowest common ancestor sheet between two sheets.""" + """Return the lowest common ancestor (LCA) of two sheets.""" path1 = self.get_sheet_hierarchy_path(sheet1) path2 = self.get_sheet_hierarchy_path(sheet2) - - # Find common prefix common_path = [] for s1, s2 in zip(path1, path2): if s1 == s2: common_path.append(s1) else: break - return common_path[-1] if common_path else None - def legalize_name(self, name: str, is_filename: bool = False) -> str: - """Convert any name into a valid Python identifier.""" - # Remove leading slashes and spaces + """Return a version of name that is a legal Python identifier.""" name = name.lstrip('/ ') - - # Handle trailing + or - if name.endswith('+'): name = name[:-1] + '_p' elif name.endswith('-'): name = name[:-1] + '_n' - - # Handle leading + or - if name.startswith('+'): name = '_p_' + name[1:] elif name.startswith('-'): name = '_n_' + name[1:] - - # Convert remaining non-alphanumeric chars to underscores legalized = re.sub(r'[^a-zA-Z0-9_]', '_', name) - - # Ensure it starts with a letter or underscore - if legalized[0].isdigit(): + if legalized and legalized[0].isdigit(): legalized = '_' + legalized - return legalized def component_to_skidl(self, comp: object) -> str: - """Convert component to SKiDL instantiation with all properties.""" - ref = comp.ref # Keep original reference + """Return a SKiDL instantiation string for a component.""" + ref = comp.ref props = [] - - # Basic properties - props.append(f"'{comp.lib}'") # Library - props.append(f"'{comp.name}'") # Part name - - # Add value if present + props.append(f"'{comp.lib}'") + props.append(f"'{comp.name}'") if comp.value: props.append(f"value='{comp.value}'") - - # Add footprint if present if comp.footprint: props.append(f"footprint='{comp.footprint}'") - - # Add description if present desc = next((p.value for p in comp.properties if p.name == 'Description'), None) if desc: props.append(f"description='{desc}'") - - # Add tag for reference - props.append(f"ref='{ref}'") # Preserve reference designator - - # Add all additional properties from netlist as part fields + props.append(f"ref='{ref}'") extra_fields = {} if hasattr(comp, 'properties'): for prop in comp.properties: @@ -129,48 +102,32 @@ def component_to_skidl(self, comp: object) -> str: extra_fields[prop.name] = prop.value if extra_fields: props.append(f"fields={repr(extra_fields)}") - - # Join all properties return f"{self.tab}{self.legalize_name(ref)} = Part({', '.join(props)})\n" def net_to_skidl(self, net: object, sheet: Sheet) -> str: - """Convert net to SKiDL connections.""" + """Return a SKiDL connection string for a net within a given sheet.""" net_name = self.legalize_name(net.name) if net_name.startswith('unconnected'): return "" - pins = [] for pin in net.pins: if any(comp.ref == pin.ref for comp in sheet.components): - comp = self.legalize_name(pin.ref) - pins.append(f"{comp}['{pin.num}']") - + comp_name = self.legalize_name(pin.ref) + pins.append(f"{comp_name}['{pin.num}']") if pins: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" def find_optimal_net_origin(self, used_sheets, net_name): - """ - Determine the optimal sheet to create a net based on hierarchy. - Returns (origin_sheet, paths_to_children) where: - - origin_sheet is the sheet where the net should be defined - - paths_to_children are the paths for passing the net to child sheets - """ - # If net is only used in one sheet, create it there + """Determine the sheet where the net should be defined and list paths for child usage.""" if len(used_sheets) == 1: return list(used_sheets)[0], [] - - # Get sheet paths for all usage points - sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) - for sheet in used_sheets} - - # Find the lowest common ancestor that is also in the used_sheets + sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) for sheet in used_sheets} common_path = None for path in sheet_paths.values(): if common_path is None: common_path = path else: - # Find common prefix new_common = [] for s1, s2 in zip(common_path, path): if s1 == s2: @@ -178,11 +135,7 @@ def find_optimal_net_origin(self, used_sheets, net_name): else: break common_path = new_common - - # Get the shallowest sheet that actually uses this net origin_sheet = common_path[-1] if common_path else '' - - # Build paths from origin to children that use this net paths = [] for sheet in used_sheets: if sheet != origin_sheet: @@ -190,40 +143,27 @@ def find_optimal_net_origin(self, used_sheets, net_name): try: start_idx = path.index(origin_sheet) child_path = path[start_idx:] - if len(child_path) > 1: # Only add if there's a path to traverse + if len(child_path) > 1: paths.append(child_path) except ValueError: continue - return origin_sheet, paths def extract_sheet_info(self): - """Build sheet hierarchy from netlist.""" + """Populate self.sheets with Sheet objects built from the netlist.""" active_logger.info("=== Extracting Sheet Info ===") - - # Create name-to-path mapping for sheet lookup. self.sheet_name_to_path = {} - - # First pass: Create all sheets. for sheet in self.netlist.sheets: - # Strip leading/trailing slashes. original_name = sheet.name.strip('/') - # If empty, then use "main". if not original_name: original_name = "main" - # Use the last element as the local name. name = original_name.split('/')[-1] - # Determine the parent based on hierarchy. - # If there is a '/' in the original name, then parent is everything before the last slash. - # Otherwise, if this sheet is not "main", force its parent to be "main". if '/' in original_name: parent = '/'.join(original_name.split('/')[:-1]) - # If parent becomes empty, set to main. if not parent: parent = "main" else: parent = "main" if name != "main" else None - self.sheets[original_name] = Sheet( number=sheet.number, name=name, @@ -234,18 +174,13 @@ def extract_sheet_info(self): parent=parent, children=[] ) - - # Store mapping from local name to full hierarchical path. self.sheet_name_to_path[name] = original_name - - # Second pass: Build parent-child relationships. for sheet in self.sheets.values(): if sheet.parent: parent_sheet = self.sheets.get(sheet.parent) if parent_sheet: parent_sheet.children.append(sheet.path) else: - # In case the parent sheet doesn't exist in our mapping, we create a root. root = self.sheets.get("main") if not root: root = Sheet( @@ -261,16 +196,14 @@ def extract_sheet_info(self): root.children.append(sheet.path) def assign_components_to_sheets(self): - """Assign components to their respective sheets.""" + """Assign each component from the netlist to its appropriate sheet.""" active_logger.info("=== Assigning Components to Sheets ===") unassigned_components = [] sheet_not_found = set() - for comp in self.netlist.parts: sheet_name = self.get_sheet_path(comp) if sheet_name: sheet_found = False - # Try to match against the sheet's local name for path, sheet in self.sheets.items(): if sheet_name == sheet.name: sheet.components.append(comp) @@ -281,11 +214,9 @@ def assign_components_to_sheets(self): unassigned_components.append(comp) else: unassigned_components.append(comp) - if sheet_not_found: active_logger.warning(f"Sheets not found in netlist: {sorted(sheet_not_found)}") active_logger.warning("Components in these sheets will be assigned to root level") - if unassigned_components: active_logger.warning(f"Found {len(unassigned_components)} unassigned components") root = self.sheets.get("main", None) @@ -302,12 +233,10 @@ def assign_components_to_sheets(self): root.components.extend(unassigned_components) def analyze_nets(self): - """Analyze nets to determine hierarchy and relationships between sheets.""" + """Analyze net usage to determine, for each net, which sheet is its origin and in which sheets it is used. + Then classify nets in each sheet as local (defined in the sheet) or imported (to be passed as a parameter).""" active_logger.info("=== Starting Net Analysis ===") - - # Map net usage: net_name -> { sheet_path: set(pins) } net_usage = defaultdict(lambda: defaultdict(set)) - active_logger.info("1. Mapping Net Usage Across Sheets:") for net in self.netlist.nets: active_logger.debug(f"\nAnalyzing net: {net.name}") @@ -320,266 +249,212 @@ def analyze_nets(self): if sheet_path: net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") - else: - active_logger.warning(f"Sheet not found for component {comp.ref}: {sheet_name}") - active_logger.info("2. Analyzing Net Origins and Hierarchy:") net_hierarchy = {} for net_name, sheet_usages in net_usage.items(): active_logger.debug(f"\nNet: {net_name}") used_sheets = set(sheet_usages.keys()) - - # Determine optimal origin by finding the lowest common ancestor among all sheets that use this net. origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) - net_hierarchy[net_name] = { 'origin_sheet': origin_sheet, 'used_in_sheets': used_sheets, 'path_to_children': paths_to_children } - active_logger.debug(f" - Origin sheet: {origin_sheet}") active_logger.debug(f" - Used in sheets: {used_sheets}") active_logger.debug(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") - - # Third pass: Classify nets for each sheet. + # Now classify nets for each sheet. for sheet in self.sheets.values(): - sheet.local_nets.clear() - sheet.imported_nets.clear() - - active_logger.debug(f"\nSheet: {sheet.name}") - for net_name, hierarchy in net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - - # If this sheet is the origin, mark net as local. - if hierarchy['origin_sheet'] == sheet.path: - sheet.local_nets.add(net_name) - active_logger.debug(f" - Local net: {net_name}") - # If this sheet is among those that use the net but not the origin, mark it as imported. - elif sheet.path in hierarchy['used_in_sheets']: - sheet.imported_nets.add(net_name) - active_logger.debug(f" - Imported net: {net_name}") - else: - for path in hierarchy['path_to_children']: - if sheet.path in path: - sheet.imported_nets.add(net_name) - active_logger.debug(f" - Imported net (path): {net_name}") - break - + # If a sheet has no parent (i.e. is top-level), we want it to be the net origin for all nets it uses. + # So we force its imported nets to be empty. + if sheet.parent is None: + sheet.imported_nets = set() + else: + sheet.local_nets.clear() + sheet.imported_nets.clear() + active_logger.debug(f"\nSheet: {sheet.name}") + for net_name, hierarchy in net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + # If the net originates in this sheet, mark it as local. + if hierarchy['origin_sheet'] == sheet.path: + sheet.local_nets.add(net_name) + # Otherwise, if this sheet uses the net, mark it as imported. + elif sheet.path in hierarchy['used_in_sheets']: + sheet.imported_nets.add(net_name) + else: + for path in hierarchy['path_to_children']: + if sheet.path in path: + sheet.imported_nets.add(net_name) + break self.net_hierarchy = net_hierarchy self.net_usage = net_usage def create_main_file(self, output_dir: str): - """Create the main.py file.""" + """Generate the main.py file. (This file is not itself a subcircuit and will simply call the top‐level subcircuits.)""" code = [ "# -*- coding: utf-8 -*-\n", "from skidl import *\n" ] - - # Import only top-level modules (those with no parent except main itself). + # Import all sheets that are direct children of main (or have parent == 'main'). for sheet in self.sheets.values(): - if (not sheet.parent) and sheet.name != 'main': + if (sheet.parent is None or sheet.parent == 'main') and sheet.name != 'main': module_name = self.legalize_name(sheet.name) code.append(f"from {module_name} import {module_name}\n") - code.extend([ - "\ndef main():\n", + "\n\ndef main():\n", + f"{self.tab}# Create global nets\n" ]) - - # Build a set of top-level nets. + # Global nets: those whose origin is in main or that are used in more than one top-level sheet. top_level_nets = set() for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - # If the net originates in "main" or is used across multiple top-level sheets, - # mark it as global. - if (hierarchy['origin_sheet'] == "main" or - len([s for s in hierarchy['used_in_sheets'] if not self.sheets[s].parent]) > 1): + # A net is global if its origin is main or if it is used in more than one sheet with no parent. + top_usage = [s for s in hierarchy['used_in_sheets'] if (self.sheets[s].parent is None or self.sheets[s].parent=='main')] + if hierarchy['origin_sheet'] == "main" or len(top_usage) > 1: top_level_nets.add(net_name) - if top_level_nets: - code.append(f"{self.tab}# Create global nets\n") for net_name in sorted(top_level_nets): legal_name = self.legalize_name(net_name) code.append(f"{self.tab}{legal_name} = Net('{net_name}')\n") code.append("\n") - - # Call top-level subcircuits. + # Call each top-level subcircuit unconditionally. code.append(f"{self.tab}# Create subcircuits\n") for sheet in self.get_hierarchical_order(): - # Only call subcircuits that are top-level (or have no parent) and are not "main" itself. - if not sheet.parent and sheet.name != 'main': + if sheet.name == 'main': + continue + if sheet.parent is None or sheet.parent == 'main': func_name = self.legalize_name(sheet.name) - needed_nets = [] - for net_name in sorted(sheet.imported_nets): - if not net_name.startswith('unconnected'): - # Verify that the net is indeed used in the sheet or its children. - sheet_path = self.get_sheet_hierarchy_path(sheet.path) - net_usage_sheets = self.net_usage.get(net_name, {}) - if any(usage_sheet in sheet_path for usage_sheet in net_usage_sheets): - needed_nets.append(self.legalize_name(net_name)) - code.append(f"{self.tab}{func_name}({', '.join(needed_nets)})\n") - else: - code.append(f"{self.tab}{self.legalize_name(sheet.name)}()\n") - + # For top-level sheets, we assume all nets are defined locally so no parameters are passed. + code.append(f"{self.tab}{func_name}()\n") + # Do not insert a recursive call to main(). code.extend([ "\nif __name__ == \"__main__\":\n", f"{self.tab}main()\n", f"{self.tab}generate_netlist()\n" ]) - main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) def generate_sheet_code(self, sheet: Sheet) -> str: - """Generate SKiDL code for a sheet.""" + """Generate the SKiDL code for a given sheet.""" code = [ "# -*- coding: utf-8 -*-\n", "from skidl import *\n" ] - # Import child subcircuits. for child_path in sheet.children: child_sheet = self.sheets[child_path] module_name = self.legalize_name(child_sheet.name) code.append(f"from {module_name} import {module_name}\n") - code.append("\n@subcircuit\n") - - # Determine required nets: these are nets used in this sheet but not created locally. + # Determine required nets: nets used in this sheet but not defined here. required_nets = set() for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - origin = hierarchy['origin_sheet'] - if sheet.path in hierarchy['used_in_sheets'] and origin != sheet.path: + # If the net is used in the sheet and it does not originate here, + # then it should be passed in. + if sheet.path in hierarchy['used_in_sheets'] and hierarchy['origin_sheet'] != sheet.path: required_nets.add(net_name) else: - # Also check if any path from the origin to a child of this sheet passes through this sheet. for path in hierarchy['path_to_children']: if sheet.path in path: required_nets.add(net_name) break - - # Legalize and sort parameter names. params = [self.legalize_name(net) for net in sorted(required_nets)] func_name = self.legalize_name(sheet.name) code.append(f"def {func_name}({', '.join(params)}):\n") - - # Create local nets: nets for which this sheet is the origin. + # Create local nets (nets whose origin is in this sheet). local_nets = [] for net_name, hierarchy in self.net_hierarchy.items(): - if (not net_name.startswith('unconnected') and - hierarchy['origin_sheet'] == sheet.path): + if net_name.startswith('unconnected'): + continue + if hierarchy['origin_sheet'] == sheet.path: local_nets.append(net_name) - if local_nets: code.append(f"{self.tab}# Local nets\n") for net in sorted(local_nets): legal_name = self.legalize_name(net) code.append(f"{self.tab}{legal_name} = Net('{net}')\n") code.append("\n") - - # Components section. + # Insert component instantiations. if sheet.components: code.append(f"{self.tab}# Components\n") for comp in sorted(sheet.components, key=lambda x: x.ref): code.append(self.component_to_skidl(comp)) code.append("\n") - # Call child subcircuits. if sheet.children: code.append(f"{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: child_sheet = self.sheets[child_path] child_func_name = self.legalize_name(child_sheet.name) + # Determine parameters for the child subcircuit by collecting nets child_nets = [] for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - origin = hierarchy['origin_sheet'] - if child_sheet.path in hierarchy['used_in_sheets']: - sheet_path = self.get_sheet_hierarchy_path(sheet.path) - if origin in sheet_path: - child_nets.append(net_name) - for path in hierarchy['path_to_children']: - if child_sheet.path in path and sheet.path == origin: - child_nets.append(net_name) + if child_sheet.path in hierarchy['used_in_sheets'] and hierarchy['origin_sheet'] != child_sheet.path: + child_nets.append(net_name) + else: + for path in hierarchy['path_to_children']: + if child_sheet.path in path: + child_nets.append(net_name) + break child_params = [self.legalize_name(net) for net in sorted(set(child_nets))] code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") - - # Connections. + # Insert connections. if sheet.components: code.append(f"\n{self.tab}# Connections\n") for net in self.netlist.nets: conn = self.net_to_skidl(net, sheet) if conn: code.append(conn) - code.append(f"{self.tab}return\n") - return "".join(code) - def get_hierarchical_order(self): - """Return sheets in dependency order.""" + """Return the sheets in dependency order (children processed before their parents).""" ordered = [] visited = set() - + def process_sheet(sheet, stack=None): if stack is None: stack = set() - - # Detect cycles if sheet.path in stack: raise ValueError(f"Cyclic dependency detected with sheet: {sheet.path}") - - # Skip if already fully processed if sheet.path in visited: return - stack.add(sheet.path) - - # Process hierarchy bottom-up: - # First process children recursively for child_path in sheet.children: child_sheet = self.sheets[child_path] process_sheet(child_sheet, stack) - - # Then add this sheet if not already added if sheet.path not in visited: ordered.append(sheet) visited.add(sheet.path) - stack.remove(sheet.path) - - # Start with independent sheets (no parent) + for sheet in self.sheets.values(): if not sheet.parent: process_sheet(sheet) - return ordered def convert(self, output_dir: str = None): - """Convert netlist to SKiDL files.""" + """Run the complete conversion and write files if output_dir is provided.""" self.extract_sheet_info() self.assign_components_to_sheets() self.analyze_nets() - if output_dir: os.makedirs(output_dir, exist_ok=True) active_logger.info(f"Generating files in {output_dir}") - - # Generate all sheet files for sheet in self.sheets.values(): if sheet.name != 'main': filename = self.legalize_name(sheet.name, is_filename=True) + '.py' sheet_path = Path(output_dir) / filename sheet_path.write_text(self.generate_sheet_code(sheet)) active_logger.debug(f"Created sheet file: {sheet_path}") - - # Create main.py last self.create_main_file(output_dir) active_logger.info("Conversion completed successfully") else: @@ -588,8 +463,7 @@ def convert(self, output_dir: str = None): return self.generate_sheet_code(main_sheet) return "" - def netlist_to_skidl(netlist_src: str, output_dir: str = None): - """Convert a KiCad netlist to SKiDL Python files.""" + """Convert a KiCad netlist to hierarchical SKiDL Python files.""" converter = HierarchicalConverter(netlist_src) - return converter.convert(output_dir) \ No newline at end of file + return converter.convert(output_dir) From f16b2776832056c8e2e0b5d74ccf12a6e6b4c12e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 19:10:27 -0800 Subject: [PATCH 48/73] replace logger statements with print statemnets --- src/skidl/netlist_to_skidl.py | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 9d061a774..0af98701c 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -11,7 +11,7 @@ from dataclasses import dataclass from typing import List, Set from kinparse import parse_netlist -from .logger import active_logger +# Removed logger import @dataclass class Sheet: @@ -151,7 +151,7 @@ def find_optimal_net_origin(self, used_sheets, net_name): def extract_sheet_info(self): """Populate self.sheets with Sheet objects built from the netlist.""" - active_logger.info("=== Extracting Sheet Info ===") + print("=== Extracting Sheet Info ===") self.sheet_name_to_path = {} for sheet in self.netlist.sheets: original_name = sheet.name.strip('/') @@ -197,7 +197,7 @@ def extract_sheet_info(self): def assign_components_to_sheets(self): """Assign each component from the netlist to its appropriate sheet.""" - active_logger.info("=== Assigning Components to Sheets ===") + print("=== Assigning Components to Sheets ===") unassigned_components = [] sheet_not_found = set() for comp in self.netlist.parts: @@ -215,10 +215,10 @@ def assign_components_to_sheets(self): else: unassigned_components.append(comp) if sheet_not_found: - active_logger.warning(f"Sheets not found in netlist: {sorted(sheet_not_found)}") - active_logger.warning("Components in these sheets will be assigned to root level") + print(f"WARNING: Sheets not found in netlist: {sorted(sheet_not_found)}") + print("WARNING: Components in these sheets will be assigned to root level") if unassigned_components: - active_logger.warning(f"Found {len(unassigned_components)} unassigned components") + print(f"WARNING: Found {len(unassigned_components)} unassigned components") root = self.sheets.get("main", None) if not root: root = Sheet( @@ -235,11 +235,11 @@ def assign_components_to_sheets(self): def analyze_nets(self): """Analyze net usage to determine, for each net, which sheet is its origin and in which sheets it is used. Then classify nets in each sheet as local (defined in the sheet) or imported (to be passed as a parameter).""" - active_logger.info("=== Starting Net Analysis ===") + print("=== Starting Net Analysis ===") net_usage = defaultdict(lambda: defaultdict(set)) - active_logger.info("1. Mapping Net Usage Across Sheets:") + print("1. Mapping Net Usage Across Sheets:") for net in self.netlist.nets: - active_logger.debug(f"\nAnalyzing net: {net.name}") + print(f"\nAnalyzing net: {net.name}") for pin in net.pins: for comp in self.netlist.parts: if comp.ref == pin.ref: @@ -248,11 +248,11 @@ def analyze_nets(self): sheet_path = self.sheet_name_to_path.get(sheet_name) if sheet_path: net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") - active_logger.debug(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") - active_logger.info("2. Analyzing Net Origins and Hierarchy:") + print(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + print("2. Analyzing Net Origins and Hierarchy:") net_hierarchy = {} for net_name, sheet_usages in net_usage.items(): - active_logger.debug(f"\nNet: {net_name}") + print(f"\nNet: {net_name}") used_sheets = set(sheet_usages.keys()) origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) net_hierarchy[net_name] = { @@ -260,9 +260,9 @@ def analyze_nets(self): 'used_in_sheets': used_sheets, 'path_to_children': paths_to_children } - active_logger.debug(f" - Origin sheet: {origin_sheet}") - active_logger.debug(f" - Used in sheets: {used_sheets}") - active_logger.debug(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") + print(f" - Origin sheet: {origin_sheet}") + print(f" - Used in sheets: {used_sheets}") + print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") # Now classify nets for each sheet. for sheet in self.sheets.values(): # If a sheet has no parent (i.e. is top-level), we want it to be the net origin for all nets it uses. @@ -272,7 +272,7 @@ def analyze_nets(self): else: sheet.local_nets.clear() sheet.imported_nets.clear() - active_logger.debug(f"\nSheet: {sheet.name}") + print(f"\nSheet: {sheet.name}") for net_name, hierarchy in net_hierarchy.items(): if net_name.startswith('unconnected'): continue @@ -448,15 +448,15 @@ def convert(self, output_dir: str = None): self.analyze_nets() if output_dir: os.makedirs(output_dir, exist_ok=True) - active_logger.info(f"Generating files in {output_dir}") + print(f"Generating files in {output_dir}") for sheet in self.sheets.values(): if sheet.name != 'main': filename = self.legalize_name(sheet.name, is_filename=True) + '.py' sheet_path = Path(output_dir) / filename sheet_path.write_text(self.generate_sheet_code(sheet)) - active_logger.debug(f"Created sheet file: {sheet_path}") + print(f"Created sheet file: {sheet_path}") self.create_main_file(output_dir) - active_logger.info("Conversion completed successfully") + print("Conversion completed successfully") else: main_sheet = next((s for s in self.sheets.values() if not s.parent), None) if main_sheet: From ea518416b442487c4219fb21a7ceb9df9f6cf8c1 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 20:02:45 -0800 Subject: [PATCH 49/73] fixed simple circuit logic --- src/skidl/netlist_to_skidl.py | 53 ++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 0af98701c..f50fe971a 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -291,52 +291,67 @@ def analyze_nets(self): self.net_usage = net_usage def create_main_file(self, output_dir: str): - """Generate the main.py file. (This file is not itself a subcircuit and will simply call the top‐level subcircuits.)""" + """Generate the main.py file that creates nets and calls subcircuits.""" code = [ "# -*- coding: utf-8 -*-\n", "from skidl import *\n" ] - # Import all sheets that are direct children of main (or have parent == 'main'). + + # Import all sheets that are direct children of main for sheet in self.sheets.values(): if (sheet.parent is None or sheet.parent == 'main') and sheet.name != 'main': module_name = self.legalize_name(sheet.name) code.append(f"from {module_name} import {module_name}\n") + code.extend([ "\n\ndef main():\n", f"{self.tab}# Create global nets\n" ]) - # Global nets: those whose origin is in main or that are used in more than one top-level sheet. - top_level_nets = set() + + # Collect nets that need to be created at top level + global_nets = set() for net_name, hierarchy in self.net_hierarchy.items(): if net_name.startswith('unconnected'): continue - # A net is global if its origin is main or if it is used in more than one sheet with no parent. - top_usage = [s for s in hierarchy['used_in_sheets'] if (self.sheets[s].parent is None or self.sheets[s].parent=='main')] - if hierarchy['origin_sheet'] == "main" or len(top_usage) > 1: - top_level_nets.add(net_name) - if top_level_nets: - for net_name in sorted(top_level_nets): - legal_name = self.legalize_name(net_name) - code.append(f"{self.tab}{legal_name} = Net('{net_name}')\n") - code.append("\n") - # Call each top-level subcircuit unconditionally. - code.append(f"{self.tab}# Create subcircuits\n") + # A net is global if it's used in multiple top-level sheets + # or if main is determined to be its origin + if hierarchy['origin_sheet'] == 'main' or len([s for s in hierarchy['used_in_sheets'] + if (self.sheets[s].parent is None or self.sheets[s].parent=='main')]) > 1: + global_nets.add(net_name) + + # Create nets at top level + for net_name in sorted(global_nets): + legal_name = self.legalize_name(net_name) + code.append(f"{self.tab}{legal_name} = Net('{net_name}')\n") + + # Call each subcircuit with its required nets + code.append(f"\n{self.tab}# Create subcircuits\n") for sheet in self.get_hierarchical_order(): if sheet.name == 'main': continue if sheet.parent is None or sheet.parent == 'main': func_name = self.legalize_name(sheet.name) - # For top-level sheets, we assume all nets are defined locally so no parameters are passed. - code.append(f"{self.tab}{func_name}()\n") - # Do not insert a recursive call to main(). + + # Get list of nets this sheet imports (needs passed in) + params = [] + for net_name in sorted(sheet.imported_nets): + params.append(self.legalize_name(net_name)) + + # Call subcircuit function with required nets + param_str = ', '.join(params) + code.append(f"{self.tab}{func_name}({param_str})\n") + + # Add boilerplate code.extend([ "\nif __name__ == \"__main__\":\n", f"{self.tab}main()\n", f"{self.tab}generate_netlist()\n" ]) + + # Write the file main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) - + def generate_sheet_code(self, sheet: Sheet) -> str: """Generate the SKiDL code for a given sheet.""" code = [ From 71462d89f259231c72f755f7fc278f08242be138 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 20:52:36 -0800 Subject: [PATCH 50/73] hierarchies for simple and complex projects appear to be working well now --- src/skidl/netlist_to_skidl.py | 345 +++++++++++++++++++++------------- 1 file changed, 213 insertions(+), 132 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index f50fe971a..4fb6a83c4 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -118,36 +118,6 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" - def find_optimal_net_origin(self, used_sheets, net_name): - """Determine the sheet where the net should be defined and list paths for child usage.""" - if len(used_sheets) == 1: - return list(used_sheets)[0], [] - sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) for sheet in used_sheets} - common_path = None - for path in sheet_paths.values(): - if common_path is None: - common_path = path - else: - new_common = [] - for s1, s2 in zip(common_path, path): - if s1 == s2: - new_common.append(s1) - else: - break - common_path = new_common - origin_sheet = common_path[-1] if common_path else '' - paths = [] - for sheet in used_sheets: - if sheet != origin_sheet: - path = sheet_paths[sheet] - try: - start_idx = path.index(origin_sheet) - child_path = path[start_idx:] - if len(child_path) > 1: - paths.append(child_path) - except ValueError: - continue - return origin_sheet, paths def extract_sheet_info(self): """Populate self.sheets with Sheet objects built from the netlist.""" @@ -175,6 +145,9 @@ def extract_sheet_info(self): children=[] ) self.sheet_name_to_path[name] = original_name + print(f" Found sheet: original_name='{original_name}', final='{name}', parent='{parent}'") + + # Set up parent-child relationships for sheet in self.sheets.values(): if sheet.parent: parent_sheet = self.sheets.get(sheet.parent) @@ -195,6 +168,19 @@ def extract_sheet_info(self): sheet.parent = "main" root.children.append(sheet.path) + print("=== Completed extracting sheet info ===") + for sheet_path, sheet in self.sheets.items(): + print(f" sheet path='{sheet_path}', parent='{sheet.parent}', children={sheet.children}") + + def is_descendant(self, potential_child, ancestor): + """Check if one sheet is a descendant of another in the hierarchy.""" + current = potential_child + while current in self.sheets: + if current == ancestor: + return True + current = self.sheets[current].parent + return False + def assign_components_to_sheets(self): """Assign each component from the netlist to its appropriate sheet.""" print("=== Assigning Components to Sheets ===") @@ -232,63 +218,6 @@ def assign_components_to_sheets(self): self.sheets["main"] = root root.components.extend(unassigned_components) - def analyze_nets(self): - """Analyze net usage to determine, for each net, which sheet is its origin and in which sheets it is used. - Then classify nets in each sheet as local (defined in the sheet) or imported (to be passed as a parameter).""" - print("=== Starting Net Analysis ===") - net_usage = defaultdict(lambda: defaultdict(set)) - print("1. Mapping Net Usage Across Sheets:") - for net in self.netlist.nets: - print(f"\nAnalyzing net: {net.name}") - for pin in net.pins: - for comp in self.netlist.parts: - if comp.ref == pin.ref: - sheet_name = self.get_sheet_path(comp) - if sheet_name: - sheet_path = self.sheet_name_to_path.get(sheet_name) - if sheet_path: - net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") - print(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") - print("2. Analyzing Net Origins and Hierarchy:") - net_hierarchy = {} - for net_name, sheet_usages in net_usage.items(): - print(f"\nNet: {net_name}") - used_sheets = set(sheet_usages.keys()) - origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) - net_hierarchy[net_name] = { - 'origin_sheet': origin_sheet, - 'used_in_sheets': used_sheets, - 'path_to_children': paths_to_children - } - print(f" - Origin sheet: {origin_sheet}") - print(f" - Used in sheets: {used_sheets}") - print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") - # Now classify nets for each sheet. - for sheet in self.sheets.values(): - # If a sheet has no parent (i.e. is top-level), we want it to be the net origin for all nets it uses. - # So we force its imported nets to be empty. - if sheet.parent is None: - sheet.imported_nets = set() - else: - sheet.local_nets.clear() - sheet.imported_nets.clear() - print(f"\nSheet: {sheet.name}") - for net_name, hierarchy in net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - # If the net originates in this sheet, mark it as local. - if hierarchy['origin_sheet'] == sheet.path: - sheet.local_nets.add(net_name) - # Otherwise, if this sheet uses the net, mark it as imported. - elif sheet.path in hierarchy['used_in_sheets']: - sheet.imported_nets.add(net_name) - else: - for path in hierarchy['path_to_children']: - if sheet.path in path: - sheet.imported_nets.add(net_name) - break - self.net_hierarchy = net_hierarchy - self.net_usage = net_usage def create_main_file(self, output_dir: str): """Generate the main.py file that creates nets and calls subcircuits.""" @@ -352,83 +281,235 @@ def create_main_file(self, output_dir: str): main_path = Path(output_dir) / "main.py" main_path.write_text("".join(code)) + def find_optimal_net_origin(self, used_sheets, net_name): + """Determine the sheet where the net should be defined and list paths for child usage. + + Args: + used_sheets: Set of sheet paths where the net is used + net_name: Name of the net being analyzed + + Returns: + Tuple of (origin_sheet, paths_to_children) where: + - origin_sheet is the sheet path where the net should be defined + - paths_to_children is a list of paths showing how the net flows to child sheets + """ + if len(used_sheets) == 1: + return list(used_sheets)[0], [] + + # Get hierarchy paths for all sheets using this net + sheet_paths = {sheet: self.get_sheet_hierarchy_path(sheet) for sheet in used_sheets} + + # Find sheets that could "own" the net by containing all its usage + origin_candidates = set() + for sheet in used_sheets: + # Get all sheets that use this net below current sheet in hierarchy + child_usage = {s for s in used_sheets + if any(sheet == p for p in sheet_paths[s])} + if len(child_usage) == len(used_sheets): + origin_candidates.add(sheet) + + # If we found candidate owners, pick the most specific (deepest) one + if origin_candidates: + max_depth = 0 + lowest_common = None + for candidate in origin_candidates: + depth = len(sheet_paths[candidate]) + if depth > max_depth: + max_depth = depth + lowest_common = candidate + else: + # Fall back to basic lowest common ancestor + common_prefix = [] + first_path = sheet_paths[list(used_sheets)[0]] + for i in range(len(first_path)): + if all(len(p) > i and p[i] == first_path[i] + for p in sheet_paths.values()): + common_prefix.append(first_path[i]) + else: + break + lowest_common = common_prefix[-1] if common_prefix else 'main' + + # Generate paths from origin to sheets that need the net + paths = [] + for sheet in used_sheets: + if sheet != lowest_common: + path = sheet_paths[sheet] + try: + start_idx = path.index(lowest_common) + child_path = path[start_idx:] + if len(child_path) > 1: + paths.append(child_path) + except ValueError: + continue + + return lowest_common, paths + + def analyze_nets(self): + """Analyze net usage to determine origins and required connections.""" + print("=== Starting Net Analysis ===") + net_usage = defaultdict(lambda: defaultdict(set)) + + print("1. Mapping Net Usage Across Sheets:") + # Map which nets are used in which sheets + for net in self.netlist.nets: + print(f"\nAnalyzing net: {net.name}") + for pin in net.pins: + for comp in self.netlist.parts: + if comp.ref == pin.ref: + sheet_name = self.get_sheet_path(comp) + if sheet_name: + sheet_path = self.sheet_name_to_path.get(sheet_name) + if sheet_path: + net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") + print(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + + print("2. Analyzing Net Origins and Hierarchy:") + net_hierarchy = {} + for net_name, sheet_usages in net_usage.items(): + print(f"\nNet: {net_name}") + used_sheets = set(sheet_usages.keys()) + origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) + net_hierarchy[net_name] = { + 'origin_sheet': origin_sheet, + 'used_in_sheets': used_sheets, + 'path_to_children': paths_to_children + } + print(f" - Origin sheet: {origin_sheet}") + print(f" - Used in sheets: {used_sheets}") + print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") + + print("3. Classifying local vs imported nets:") + # Clear any existing net classifications + for sheet in self.sheets.values(): + sheet.local_nets.clear() + sheet.imported_nets.clear() + + # Process each sheet + for sheet_path, sheet in self.sheets.items(): + print(f" Checking sheet: '{sheet.name}', path='{sheet_path}' with parent='{sheet.parent}'") + + # Top level sheet doesn't import nets + if sheet.parent is None: + print(f" Top-level sheet: '{sheet_path}' => clearing imported_nets.") + continue + + for net_name, hierarchy in net_hierarchy.items(): + if net_name.startswith('unconnected'): + continue + + # Skip nets not used in or below this sheet + if sheet_path not in hierarchy['used_in_sheets'] and \ + not any(sheet_path in path for path in hierarchy['path_to_children']): + continue + + # Determine if net should be local or imported + is_local = False + + # Net originates in this sheet + if hierarchy['origin_sheet'] == sheet_path: + is_local = True + + # Net is only used within this sheet's hierarchy + elif all(self.is_descendant(used_sheet, sheet_path) + for used_sheet in hierarchy['used_in_sheets']): + is_local = True + + # Handle special case nets (like power/ground) that need to flow down + elif any(sheet_path in path for path in hierarchy['path_to_children']): + is_local = False + + if is_local: + sheet.local_nets.add(net_name) + print(f" Net {net_name} is local (origin in this sheet).") + else: + sheet.imported_nets.add(net_name) + print(f" Net {net_name} is imported here.") + + self.net_hierarchy = net_hierarchy + self.net_usage = net_usage + print("=== Completed net analysis ===") + + # Print summary for each sheet + for sheet_path, sheet in self.sheets.items(): + print(f"Sheet '{sheet_path}': local_nets={sheet.local_nets}, imported_nets={sheet.imported_nets}") + def generate_sheet_code(self, sheet: Sheet) -> str: """Generate the SKiDL code for a given sheet.""" + print(f"=== generate_sheet_code for sheet '{sheet.name}' ===") + code = [ "# -*- coding: utf-8 -*-\n", "from skidl import *\n" ] - # Import child subcircuits. + + # Import child subcircuits for child_path in sheet.children: child_sheet = self.sheets[child_path] module_name = self.legalize_name(child_sheet.name) code.append(f"from {module_name} import {module_name}\n") + + # Start function definition code.append("\n@subcircuit\n") - # Determine required nets: nets used in this sheet but not defined here. - required_nets = set() - for net_name, hierarchy in self.net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - # If the net is used in the sheet and it does not originate here, - # then it should be passed in. - if sheet.path in hierarchy['used_in_sheets'] and hierarchy['origin_sheet'] != sheet.path: - required_nets.add(net_name) - else: - for path in hierarchy['path_to_children']: - if sheet.path in path: - required_nets.add(net_name) - break - params = [self.legalize_name(net) for net in sorted(required_nets)] + + # Determine required nets that need to be passed in + required_nets = [] + for net_name in sorted(sheet.imported_nets): + # Verify net really needs to be imported + # (used by this sheet or needed by children) + if (sheet.path in self.net_usage[net_name] or + any(child in self.net_usage[net_name] + for child in sheet.children)): + required_nets.append(self.legalize_name(net_name)) + func_name = self.legalize_name(sheet.name) - code.append(f"def {func_name}({', '.join(params)}):\n") - # Create local nets (nets whose origin is in this sheet). - local_nets = [] - for net_name, hierarchy in self.net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - if hierarchy['origin_sheet'] == sheet.path: - local_nets.append(net_name) + code.append(f"def {func_name}({', '.join(required_nets)}):\n") + + # Create local nets + local_nets = sorted(sheet.local_nets) if local_nets: code.append(f"{self.tab}# Local nets\n") - for net in sorted(local_nets): + for net in local_nets: legal_name = self.legalize_name(net) code.append(f"{self.tab}{legal_name} = Net('{net}')\n") code.append("\n") - # Insert component instantiations. + + # Create components if sheet.components: code.append(f"{self.tab}# Components\n") for comp in sorted(sheet.components, key=lambda x: x.ref): code.append(self.component_to_skidl(comp)) code.append("\n") - # Call child subcircuits. + + # Create subcircuits if sheet.children: code.append(f"{self.tab}# Hierarchical subcircuits\n") for child_path in sheet.children: - child_sheet = self.sheets[child_path] - child_func_name = self.legalize_name(child_sheet.name) - # Determine parameters for the child subcircuit by collecting nets - child_nets = [] - for net_name, hierarchy in self.net_hierarchy.items(): - if net_name.startswith('unconnected'): - continue - if child_sheet.path in hierarchy['used_in_sheets'] and hierarchy['origin_sheet'] != child_sheet.path: - child_nets.append(net_name) - else: - for path in hierarchy['path_to_children']: - if child_sheet.path in path: - child_nets.append(net_name) - break - child_params = [self.legalize_name(net) for net in sorted(set(child_nets))] - code.append(f"{self.tab}{child_func_name}({', '.join(child_params)})\n") - # Insert connections. + child = self.sheets[child_path] + child_func = self.legalize_name(child.name) + + # Determine which nets to pass to child + child_params = [] + for net_name in sorted(child.imported_nets): + if (child_path in self.net_usage[net_name] or + any(grandchild in self.net_usage[net_name] + for grandchild in child.children)): + child_params.append(self.legalize_name(net_name)) + + code.append(f"{self.tab}{child_func}({', '.join(child_params)})\n") + + # Create connections if sheet.components: code.append(f"\n{self.tab}# Connections\n") for net in self.netlist.nets: conn = self.net_to_skidl(net, sheet) if conn: code.append(conn) + code.append(f"{self.tab}return\n") - return "".join(code) + + generated_code = "".join(code) + print(f"Generated code for sheet '{sheet.name}':\n{generated_code}") + return generated_code def get_hierarchical_order(self): """Return the sheets in dependency order (children processed before their parents).""" From 67892dd7df2bc9eeda59f9384fbab69df7e79dc4 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 21:08:02 -0800 Subject: [PATCH 51/73] refactor, working still --- src/skidl/netlist_to_skidl.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index 4fb6a83c4..ba852efc9 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -118,7 +118,6 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: return f"{self.tab}{net_name} += {', '.join(pins)}\n" return "" - def extract_sheet_info(self): """Populate self.sheets with Sheet objects built from the netlist.""" print("=== Extracting Sheet Info ===") @@ -218,7 +217,6 @@ def assign_components_to_sheets(self): self.sheets["main"] = root root.components.extend(unassigned_components) - def create_main_file(self, output_dir: str): """Generate the main.py file that creates nets and calls subcircuits.""" code = [ From dbf5c0c4efaf28d8c38a636fe7336c8c49daf196 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Sun, 2 Feb 2025 21:10:47 -0800 Subject: [PATCH 52/73] remove debug script --- query_gen.py | 210 --------------------------------------------------- 1 file changed, 210 deletions(-) delete mode 100644 query_gen.py diff --git a/query_gen.py b/query_gen.py deleted file mode 100644 index 3d1daaf2b..000000000 --- a/query_gen.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 - -#============================================================================== -# QUICK EDIT CONFIGURATION - Modify these values as needed -#============================================================================== - -# Where to look for files -ROOT_DIRECTORY = "/Users/shanemattner/Desktop/skidl" - -# Where to save the combined output -OUTPUT_FILE = "collected_code.txt" - -# What files to collect (add or remove filenames as needed) -TARGET_FILES = [ - 'netlist_to_skidl.py', - 'main_control_board.py', - 'Block_Diagram.py', - 'Project_Architecture.py', - 'Power_Info.py', - 'Revision_History.py', - 'Coral_TPU.py', - 'ESP32S3.PY', - 'Left_Leg.py', - 'Right_Leg.py', - 'Voltage_Regulators.py', - 'main_simple_project.py', - 'esp32s3mini1.py', - '_3v3_regulator.py', - 'resistor_divider1.py', - 'test_examples.py', - 'USB.py', - 'example_kicad_project.net', - 'report.txt' -] - -# Message to add at the start of the output file -INTRO_MESSAGE = """ - -netlist_to_skidl.py is the source file for logic that converts a KiCad netlist into equivalent hierarchical SKiDL programs. - -This logic works well for a complex KiCad project. See the following files for the complex project: - - main_control_board.py - - Block_Diagram.py - - Project_Architecture.py - - Power_Info.py - - Revision_History.py - - Coral_TPU.py - - ESP32S3.PY - - Left_Leg.py - - Right_Leg.py - - Voltage_Regulators.py - -However, when converting a KiCad netlist for a simple project (see these files): - - example_kicad_project.net (netlist file being processed) - - main_simple_project.py - - esp32s3mini1.py - - _3v3_regulator.py - - resistor_divider1.py - - test_examples.py - - USB.py - - report.txt - -The logic seems to work fairly well except: -- **esp32s3mini1() Parameter Issue:** - The generated subcircuit definition for `esp32s3mini1` erroneously includes an argument named `esp32s3mini1_HW_VER`. This net is not needed as an external parameter for this subcircuit. After manually removing this argument and running the generated main file, the output netlist imports correctly into KiCad (see report.txt for import logs). The problem appears to be in the net analysis logic, where the converter mistakenly classifies the net named `/esp32s3mini1/HW_VER` as imported rather than local. The solution is to adjust the net classification so that if the net originates in the same sheet as the subcircuit (or is intended to be local), it is not passed in as a parameter. - -- **main_control_board.py Subcircuit Calls:** - In the complex project, the generated `main_control_board.py` does not call any of the subsheets. In other words, the top-level file fails to invoke the subcircuits (such as Block_Diagram, ESP32S3, Voltage_Regulators, etc.). This indicates that the logic used to decide which sheets are “top-level” (and thus should be called from main) is not detecting them properly. The net analysis and sheet‐hierarchy code must be adjusted so that the top‐level aggregator (or main sheet) correctly calls all its child subcircuits—even if some of them do not import any nets—so that the hierarchical structure is fully instantiated in the generated main file. - -- **Project_Architecture() Parameter Issue:** - The generated definition for `Project_Architecture()` (which serves as the top-level sheet for the board) erroneously includes many parameters (e.g. `_p_3_3V`, `_p_5V`, and several others). Since Project_Architecture.py is intended to be the top-level sheet (with its purpose being mainly to document the overall architecture), it should not require any externally passed parameters; it should instead define its own local nets (or rely on the global definitions created by main). This indicates that the converter’s heuristic for deciding which nets are “imported” (i.e. must be passed in as parameters) is treating nets used in Project_Architecture as imported rather than local. The solution is to modify the net classification logic so that for the top-level aggregator sheet, all nets that it uses are defined locally and no parameters are required. - -In summary, the underlying issue in both simple and complex projects is how the converter’s net analysis and sheet‐hierarchy logic decide whether a net is “imported” (and thus should appear as a function parameter) or “local” (defined inside the subcircuit). For the simple project, no parameters are passed to subcircuits (even though the design requires them), while for the complex project the top-level Project_Architecture subcircuit is erroneously given a long parameter list. The fix must be made purely through analysis (e.g. by comparing the sheet’s “origin” for a net versus its usage) rather than by hardcoding specific net or sheet names. - -**Where to Look for a Solution:** - -- **netlist_to_skidl.py:** - • In the `analyze_nets()` method, review how nets are classified as local versus imported. The logic that checks whether a net “originates” in the same sheet should be adjusted so that, for example, the `/esp32s3mini1/HW_VER` net is marked as local (thus not passed in as a parameter to `esp32s3mini1`). - • In the `create_main_file()` method, verify the conditions used to decide which sheets are “top-level” (i.e. those that should be called from main). The condition should not omit subcircuits from being called if they belong to the top-level hierarchy. - -- **Sheet Hierarchy:** - Examine how sheet parent/child relationships are built in `extract_sheet_info()` and how they influence the net analysis. Ensure that a top-level aggregator like Project_Architecture is treated as “local” for its nets, so that no external parameters are needed. - -- **Subcircuit Calls in main_simple_project.py and main_control_board.py:** - Compare the generated main files for the simple and complex projects. The simple project should have calls like - ```python - esp32s3mini1(_p_3V3, _3v3_monitor, _5v_monitor, D_p, D_n, GND) - _3v3_regulator(_p_3V3, _p_5V, _3v3_monitor, _5v_monitor, GND) - USB(_p_5V, D_p, D_n, GND) - -""" - - -#============================================================================== -# Script Implementation - No need to modify below this line -#============================================================================== - -""" -File Collector for Query Building - -This script combines specific files into a single output file to help build -queries when iterating on software development. Edit the CONFIGURATION section -at the top to customize which files to collect. -""" - -import os -from typing import List -from dataclasses import dataclass - -@dataclass -class FileCollectorConfig: - """Configuration class to store all script parameters""" - root_directory: str - output_filename: str - target_filenames: List[str] - intro_message: str - -def create_config_from_settings() -> FileCollectorConfig: - """Creates configuration object from the settings defined at the top of the script""" - return FileCollectorConfig( - root_directory=ROOT_DIRECTORY, - output_filename=OUTPUT_FILE, - target_filenames=TARGET_FILES, - intro_message=INTRO_MESSAGE - ) - -def is_target_file(filename: str, target_files: List[str]) -> bool: - """ - Check if a filename matches one of our target filenames. - - Args: - filename: Name of the file to check - target_files: List of target filenames to match against - """ - return os.path.basename(filename) in target_files - -def find_target_files(config: FileCollectorConfig) -> List[str]: - """ - Search for target files in the root directory. - - Args: - config: Configuration object containing search parameters - - Returns: - List[str]: List of full file paths for matching files - """ - collected_files = [] - - # Walk through the directory tree - for dirpath, _, filenames in os.walk(config.root_directory): - for filename in filenames: - if is_target_file(filename, config.target_filenames): - full_path = os.path.join(dirpath, filename) - if os.path.isfile(full_path): - collected_files.append(full_path) - - return sorted(collected_files) - -def write_combined_file(collected_files: List[str], config: FileCollectorConfig) -> None: - """ - Write all collected file contents to a single output file. - - Args: - collected_files: List of file paths to combine - config: Configuration object containing output settings - """ - with open(config.output_filename, 'w') as out_file: - # Write the introduction message - out_file.write(config.intro_message + "\n") - - # Process each collected file - total_lines = 0 - for file_path in collected_files: - try: - # Read and write each file's contents with clear separation - with open(file_path, 'r') as input_file: - content = input_file.read() - filename = os.path.basename(file_path) - - # Add clear separators around file content - out_file.write(f"\n/* Begin of file: {filename} */\n") - out_file.write(content) - out_file.write(f"\n/* End of file: {filename} */\n") - - # Print statistics for monitoring - num_lines = len(content.splitlines()) - total_lines += num_lines - print(f"{filename}: {num_lines} lines") - - except Exception as e: - print(f"Error processing {file_path}: {e}") - print(f"Total lines written: {total_lines}") - -def main(): - """Main execution function""" - # Create configuration from settings - config = create_config_from_settings() - - # Find all matching files - collected_files = find_target_files(config) - - # Combine files into output - write_combined_file(collected_files, config) - - # Print summary - print(f"\nProcessed {len(collected_files)} files") - print(f"Output saved to: {config.output_filename}") - -if __name__ == "__main__": - main() From 63fa615ea8f949a2e3d69c0e3e013e8e881fb1b1 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 3 Feb 2025 07:18:06 -0800 Subject: [PATCH 53/73] working llm analysis again --- kicad_skidl_llm.py | 169 +++++++++++++++++++++++----------- src/skidl/circuit_analyzer.py | 101 +++++++++----------- 2 files changed, 160 insertions(+), 110 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index e7bf3962b..4d740d948 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -20,9 +20,19 @@ Key Features: - Safe discovery and analysis of SKiDL circuits using AST parsing - Support for both single-file and multi-file SKiDL projects - - Flexible LLM backend selection (OpenRouter API or local Ollama) + - Flexible LLM backend selection: + * OpenRouter (cloud-based): Note that model names must be set according + to the OpenRouter naming standard. + * Ollama (local): If using this backend, ensure Ollama is installed locally. - Comprehensive error handling and reporting +Additional Notes for Novice Users: + - Make sure you have KiCad 7.0+ installed. You must also point the script to the + correct location of your KiCad CLI executable (using --kicad-cli) if it is not in the default path. + - For LLM analysis using OpenRouter, you must provide a valid API key with --api-key. + - Ensure that any LLM model name provided (via --model) adheres to the naming conventions + required by the selected backend. + Usage Examples: # Generate netlist and SKiDL project from schematic python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist --generate-skidl @@ -33,19 +43,22 @@ # Generate SKiDL from netlist and analyze using Ollama python kicad_skidl_llm.py --netlist circuit.net --generate-skidl --analyze --backend ollama + # Dump temporary analysis file for inspection + python kicad_skidl_llm.py --skidl-source myproject/ --analyze --api-key YOUR_KEY --dump-temp + Known Limitations: - 1. Memory usage may be high for projects with many subcircuits - 2. Python path handling may need adjustment for complex project structures - 3. Circular dependencies in SKiDL projects may cause issues - 4. File encoding issues possible with non-UTF8 files - 5. Large projects may hit API rate limits with cloud LLMs + 1. Memory usage may be high for projects with many subcircuits. + 2. Python path handling may need adjustment for complex project structures. + 3. Circular dependencies in SKiDL projects may cause issues. + 4. File encoding issues possible with non-UTF8 files. + 5. Large projects may hit API rate limits with cloud LLMs. Dependencies: - Python 3.7+ - KiCad 7.0+ (for schematic operations) - SKiDL - AST (standard library) - - Either OpenRouter API key or local Ollama installation + - Either OpenRouter API key (for cloud-based LLM analysis) or a local Ollama installation Author: [Your Name] Date: February 2024 @@ -61,16 +74,15 @@ import textwrap import traceback from pathlib import Path -from typing import List, Dict, Optional, Union, Tuple +from typing import List, Dict, Optional, Tuple import argparse from skidl import * - +# Custom exception to signal issues during circuit discovery and loading. class CircuitDiscoveryError(Exception): """Raised when there are issues discovering or loading circuits.""" pass - class CircuitAnalyzer: """ Handles discovery and analysis of SKiDL circuits. @@ -79,7 +91,12 @@ class CircuitAnalyzer: - Finding @subcircuit decorated functions in Python files - Safely loading and executing discovered circuits - Running LLM analysis on circuits - + + Note for LLM Analysis: + The default LLM analysis uses OpenRouter. This means that any LLM model name + provided must follow OpenRouter's naming standards. Alternatively, you can choose + the 'ollama' backend for local analysis if you have Ollama installed. + The analyzer uses AST parsing to discover circuits without executing potentially unsafe code, then creates isolated environments for circuit execution and analysis. @@ -100,6 +117,7 @@ def find_subcircuits(directory: Path) -> List[Dict[str, str]]: List of dicts containing: - 'file': Path to Python file - 'function': Name of decorated function + - 'lineno': Line number where the function is defined Raises: CircuitDiscoveryError: If no valid Python files found @@ -138,6 +156,12 @@ def create_analysis_module( ) -> Tuple[Path, str]: """ Create a temporary Python module that imports and executes discovered subcircuits. + The generated module creates dummy SKiDL nets for any parameters required by the + subcircuit functions, allowing them to be called without errors. + + This version groups the discovered functions by file and generates explicit + import statements (rather than wildcard imports) so that functions whose names + start with an underscore (e.g. _3v3_regulator) are imported correctly. Args: subcircuits: List of subcircuit information from find_subcircuits() @@ -152,24 +176,42 @@ def create_analysis_module( CircuitDiscoveryError: If module creation fails """ analysis_file = output_dir / 'circuit_analysis.py' - try: - # Generate imports and function calls + # Generate import statements and helper function. code_lines = [ "from skidl import *\n", - "from skidl.tools import *\n" + "from skidl.tools import *\n", + "import inspect\n\n", + "# Helper function that creates dummy nets for required parameters.\n", + "def call_subcircuit(func):\n", + " # If the function is wrapped, get the original.\n", + " wrapped = getattr(func, '__wrapped__', func)\n", + " sig = inspect.signature(wrapped)\n", + " # Create a dummy net for each parameter\n", + " dummy_args = [Net(param.name) for param in sig.parameters.values()]\n", + " return func(*dummy_args)\n\n" ] - # Import each unique file containing subcircuits - unique_files = {s['file'] for s in subcircuits} - for file in unique_files: + # Group discovered functions by file. + imports_by_file = {} + for s in subcircuits: + file = s['file'] + func_name = s['function'] + if file not in imports_by_file: + imports_by_file[file] = set() + imports_by_file[file].add(func_name) + + # Generate explicit import lines for each file. + for file, functions in imports_by_file.items(): module_name = Path(file).stem - code_lines.append(f"from {module_name} import *\n") + funcs_str = ", ".join(sorted(functions)) + code_lines.append(f"from {module_name} import {funcs_str}\n") - # Create main function to execute subcircuits - code_lines.append("\ndef main():\n") - for circuit in subcircuits: - code_lines.append(f" {circuit['function']}()\n") + # Create main function to execute each subcircuit via call_subcircuit. + code_lines.append("\n\ndef main():\n") + for s in subcircuits: + func_name = s['function'] + code_lines.append(f" call_subcircuit({func_name})\n") code = ''.join(code_lines) @@ -190,22 +232,24 @@ def analyze_circuits( api_key: Optional[str] = None, backend: str = 'openrouter', model: Optional[str] = None, - prompt: Optional[str] = None + prompt: Optional[str] = None, + dump_temp: bool = False ) -> dict: """ Analyze circuits from a SKiDL source (file or directory). Handles both single file and project directory cases: - Single file: Imports and executes directly - - Directory: Finds @subcircuit functions and creates analysis module + - Directory: Finds @subcircuit functions and creates an analysis module Args: source: Path to SKiDL file or project directory output_file: Where to save analysis results - api_key: API key for cloud LLM service + api_key: API key for cloud LLM service (required for openrouter) backend: 'openrouter' or 'ollama' - model: Model name for selected backend + model: Model name for selected backend (model names must adhere to the backend's naming standard) prompt: Custom analysis prompt + dump_temp: If True, do not delete the temporary analysis module and output its location. Returns: Analysis results dictionary containing: @@ -220,7 +264,7 @@ def analyze_circuits( RuntimeError: For analysis failures """ if source.is_file(): - # Single file case - import directly + # Single file case - import directly. sys.path.insert(0, str(source.parent)) try: module = importlib.import_module(source.stem) @@ -239,7 +283,7 @@ def analyze_circuits( sys.path.pop(0) else: - # Directory case - find and analyze subcircuits + # Directory case - find and analyze subcircuits. try: subcircuits = CircuitAnalyzer.find_subcircuits(source) if not subcircuits: @@ -262,17 +306,20 @@ def analyze_circuits( module.main() finally: sys.path.pop(0) - try: - analysis_module.unlink() # Clean up temporary file - except Exception as e: - print(f"Warning: Failed to remove temporary module: {e}") + if dump_temp: + print(f"Temporary analysis module saved at: {analysis_module}") + else: + try: + analysis_module.unlink() # Clean up temporary file. + except Exception as e: + print(f"Warning: Failed to remove temporary module: {e}") except Exception as e: raise CircuitDiscoveryError( f"Circuit discovery/execution failed: {str(e)}" ) from e - # Run LLM analysis + # Run LLM analysis. try: analysis_kwargs = { 'output_file': output_file, @@ -297,6 +344,12 @@ def validate_kicad_cli(path: str) -> str: """ Validate that KiCad CLI exists and is executable. + NOTE for novice users: + You may need to adjust the path to point to your local KiCad installation. + For example, on macOS the default might be: + /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli + Change this value with the --kicad-cli parameter if necessary. + Args: path: Path to kicad-cli executable @@ -338,10 +391,13 @@ def parse_args() -> argparse.Namespace: # Generate SKiDL from netlist and analyze using Ollama %(prog)s --netlist circuit.net --generate-skidl --analyze --backend ollama + + # Dump the temporary analysis file for inspection: + %(prog)s --skidl-source myproject/ --analyze --api-key YOUR_KEY --dump-temp """) ) - # Input source group (mutually exclusive) + # Input source group (mutually exclusive). source_group = parser.add_mutually_exclusive_group(required=True) source_group.add_argument( '--schematic', '-s', @@ -356,7 +412,7 @@ def parse_args() -> argparse.Namespace: help='Path to SKiDL file or project directory' ) - # Operation mode flags + # Operation mode flags. parser.add_argument( '--generate-netlist', action='store_true', @@ -373,11 +429,11 @@ def parse_args() -> argparse.Namespace: help='Run LLM analysis on circuits' ) - # Optional configuration + # Optional configuration. parser.add_argument( '--kicad-cli', default="/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", - help='Path to kicad-cli executable' + help='Path to kicad-cli executable (adjust this if your KiCad installation is in a different location)' ) parser.add_argument( '--output-dir', '-o', @@ -386,17 +442,17 @@ def parse_args() -> argparse.Namespace: ) parser.add_argument( '--api-key', - help='OpenRouter API key for cloud LLM analysis' + help='OpenRouter API key for cloud LLM analysis (required for openrouter backend)' ) parser.add_argument( '--backend', choices=['openrouter', 'ollama'], default='openrouter', - help='LLM backend to use' + help='LLM backend to use (default is openrouter; note that model names must follow OpenRouter naming standards)' ) parser.add_argument( '--model', - help='LLM model name for selected backend' + help='LLM model name for selected backend (ensure the model name conforms to the backend\'s naming standard)' ) parser.add_argument( '--analysis-output', @@ -407,10 +463,15 @@ def parse_args() -> argparse.Namespace: '--analysis-prompt', help='Custom prompt for circuit analysis' ) + parser.add_argument( + '--dump-temp', + action='store_true', + help='Keep and output the temporary SKiDL analysis file for manual inspection' + ) args = parser.parse_args() - # Validate argument combinations + # Validate argument combinations. if args.generate_netlist and not args.schematic: parser.error("--generate-netlist requires --schematic") if args.generate_skidl and not (args.netlist or args.generate_netlist): @@ -433,6 +494,9 @@ def main(): The pipeline tracks state between steps and provides detailed error reporting for each stage. + Note: Ensure that you have configured the correct path for kicad-cli, + and that any LLM model names conform to the backend's naming requirements. + Raises: Various exceptions with descriptive messages for different failure modes. @@ -440,15 +504,15 @@ def main(): try: args = parse_args() - # Determine output directory + # Determine output directory. output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) - # Track current state for pipeline + # Track current state for pipeline. current_netlist = None current_skidl = None - # 1. Generate netlist if requested + # 1. Generate netlist if requested. if args.schematic and args.generate_netlist: print("\nStep 1: Generating netlist from schematic...") schematic_path = Path(args.schematic) @@ -457,10 +521,10 @@ def main(): if schematic_path.suffix != '.kicad_sch': raise ValueError(f"Input must be .kicad_sch file: {schematic_path}") - # Validate KiCad CLI + # Validate KiCad CLI; adjust the path via --kicad-cli if needed. kicad_cli = validate_kicad_cli(args.kicad_cli) - # Generate netlist + # Generate netlist using the KiCad CLI tool. netlist_path = output_dir / f"{schematic_path.stem}.net" try: subprocess.run([ @@ -480,14 +544,14 @@ def main(): current_netlist = netlist_path print(f"✓ Generated netlist: {netlist_path}") - # 2. Start from netlist if provided + # 2. Start from netlist if provided. elif args.netlist: current_netlist = Path(args.netlist) if not current_netlist.exists(): raise FileNotFoundError(f"Netlist not found: {current_netlist}") print(f"\nUsing existing netlist: {current_netlist}") - # 3. Generate SKiDL project if requested + # 3. Generate SKiDL project if requested. if current_netlist and args.generate_skidl: print("\nStep 2: Generating SKiDL project from netlist...") skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" @@ -507,14 +571,14 @@ def main(): current_skidl = skidl_dir print(f"✓ Generated SKiDL project: {skidl_dir}") - # 4. Start from SKiDL if provided + # 4. Start from SKiDL if provided. elif args.skidl_source: current_skidl = Path(args.skidl_source) if not current_skidl.exists(): raise FileNotFoundError(f"SKiDL source not found: {current_skidl}") print(f"\nUsing existing SKiDL source: {current_skidl}") - # 5. Run analysis if requested + # 5. Run analysis if requested. if args.analyze: if not current_skidl: raise ValueError("No SKiDL source available for analysis") @@ -527,7 +591,8 @@ def main(): api_key=args.api_key, backend=args.backend, model=args.model, - prompt=args.analysis_prompt + prompt=args.analysis_prompt, + dump_temp=args.dump_temp ) if results["success"]: @@ -571,4 +636,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index 53c054f0c..db6626f03 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -9,7 +9,7 @@ # API configuration OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" OLLAMA_API_URL = "http://localhost:11434/api/chat" -DEFAULT_MODEL = "anthropic/claude-3.5-haiku" +DEFAULT_MODEL = "google/gemini-flash-1.5" DEFAULT_OLLAMA_MODEL = "mistral" DEFAULT_TIMEOUT = 30 DEFAULT_TEMPERATURE = 0.7 @@ -17,20 +17,9 @@ MAX_RETRIES = 3 class SkidlCircuitAnalyzer: - """ - Circuit analyzer using Large Language Models through OpenRouter. - - Attributes: - model: Name of the OpenRouter model to use - api_key: OpenRouter API key - custom_prompt: Additional custom prompt template - analysis_flags: Dict of analysis sections to enable/disable - config: Additional configuration options - """ - def __init__( self, - model: str = DEFAULT_MODEL, + model: Optional[str] = None, api_key: Optional[str] = None, custom_prompt: Optional[str] = None, analysis_flags: Optional[Dict[str, bool]] = None, @@ -40,16 +29,7 @@ def __init__( backend: Literal["openrouter", "ollama"] = "openrouter", **kwargs ): - """ - Initialize the circuit analyzer. - - Args: - model: Name of the OpenRouter model to use - api_key: OpenRouter API key (or set OPENROUTER_API_KEY env var) - custom_prompt: Additional custom prompt template to append - analysis_flags: Dict of analysis sections to enable/disable - **kwargs: Additional configuration options - """ + """Initialize with fixed model selection logic""" self.backend = backend # Check for API key if using OpenRouter @@ -61,16 +41,13 @@ def __init__( "1. Set OPENROUTER_API_KEY environment variable\n" "2. Pass api_key parameter to analyze_with_llm" ) - if model == DEFAULT_OLLAMA_MODEL: - self.model = DEFAULT_MODEL - else: - self.model = model + # Fixed model selection logic + self.model = model if model else DEFAULT_MODEL + else: # ollama self.api_key = None - if model == DEFAULT_MODEL: - self.model = DEFAULT_OLLAMA_MODEL - else: - self.model = model + # For Ollama, use provided model or default Ollama model + self.model = model if model else DEFAULT_OLLAMA_MODEL self.timeout = timeout self.temperature = temperature @@ -192,44 +169,52 @@ def analyze_circuit( request_start = time.time() if self.backend == "openrouter": - headers = { - "Authorization": f"Bearer {self.api_key}", + # Use the OpenAI client to query OpenRouter + from openai import OpenAI # Requires the OpenAI package configured for OpenRouter + client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=self.api_key, + ) + + # Set extra headers as required + extra_headers = { "HTTP-Referer": "https://github.com/devbisme/skidl", "X-Title": "SKiDL Circuit Analyzer" } - data = { - "model": self.model, - "messages": [{"role": "user", "content": prompt}], - "temperature": self.temperature, - "max_tokens": self.max_tokens, - } + # if verbose: + # print("Sending payload to OpenRouter:") + # print({ + # "model": self.model, + # "messages": [{"role": "user", "content": prompt}], + # "temperature": self.temperature, + # "max_tokens": self.max_tokens, + # "extra_headers": extra_headers, + # }) - # Implement retries with exponential backoff for attempt in range(MAX_RETRIES): try: - response = requests.post( - OPENROUTER_API_URL, - headers=headers, - json=data, - timeout=self.timeout + completion = client.chat.completions.create( + model=self.model, + messages=[{"role": "user", "content": prompt}], + temperature=self.temperature, + max_tokens=self.max_tokens, + extra_headers=extra_headers ) - response.raise_for_status() - response_json = response.json() - analysis_text = response_json["choices"][0]["message"]["content"] + + analysis_text = completion.choices[0].message.content request_time = time.time() - request_start - # Track token usage - usage = response_json.get("usage", {}) - prompt_tokens = usage.get("prompt_tokens", 0) - completion_tokens = usage.get("completion_tokens", 0) - total_tokens = usage.get("total_tokens", 0) + # Retrieve token usage if available + prompt_tokens = completion.usage.prompt_tokens + completion_tokens = completion.usage.completion_tokens + total_tokens = completion.usage.total_tokens break - except requests.exceptions.RequestException as e: - if attempt == MAX_RETRIES - 1: # Last attempt + except Exception as e: + if attempt == MAX_RETRIES - 1: raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") time.sleep(2 ** attempt) # Exponential backoff - else: # ollama + else: # ollama backend remains unchanged data = { "model": self.model, "messages": [{"role": "user", "content": prompt}], @@ -258,7 +243,7 @@ def analyze_circuit( total_tokens = 0 break except requests.exceptions.RequestException as e: - if attempt == MAX_RETRIES - 1: # Last attempt + if attempt == MAX_RETRIES - 1: raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") time.sleep(2 ** attempt) # Exponential backoff @@ -277,7 +262,7 @@ def analyze_circuit( "total_tokens": total_tokens } - # Extract file operations to separate method + # Save analysis to file if required if output_file: self._save_analysis(output_file, analysis_text, verbose) From acf720e7df66dc56ac8216bec2cf4fbd28186b3b Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 3 Feb 2025 07:27:21 -0800 Subject: [PATCH 54/73] working ollama example --- src/skidl/circuit_analyzer.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index db6626f03..d8762351f 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -5,12 +5,13 @@ import time import os import requests +from openai import OpenAI # API configuration OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" OLLAMA_API_URL = "http://localhost:11434/api/chat" DEFAULT_MODEL = "google/gemini-flash-1.5" -DEFAULT_OLLAMA_MODEL = "mistral" +DEFAULT_OLLAMA_MODEL = "llama3.2:latest" DEFAULT_TIMEOUT = 30 DEFAULT_TEMPERATURE = 0.7 DEFAULT_MAX_TOKENS = 20000 @@ -170,7 +171,6 @@ def analyze_circuit( if self.backend == "openrouter": # Use the OpenAI client to query OpenRouter - from openai import OpenAI # Requires the OpenAI package configured for OpenRouter client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key=self.api_key, @@ -182,16 +182,6 @@ def analyze_circuit( "X-Title": "SKiDL Circuit Analyzer" } - # if verbose: - # print("Sending payload to OpenRouter:") - # print({ - # "model": self.model, - # "messages": [{"role": "user", "content": prompt}], - # "temperature": self.temperature, - # "max_tokens": self.max_tokens, - # "extra_headers": extra_headers, - # }) - for attempt in range(MAX_RETRIES): try: completion = client.chat.completions.create( From 5ee7591a27f8233edca7ae08b46fc4a384e92397 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 3 Feb 2025 07:47:14 -0800 Subject: [PATCH 55/73] replace print with active_logger --- src/skidl/circuit_analyzer.py | 23 ++++++------ src/skidl/netlist_to_skidl.py | 70 ++++++++++++++++++++--------------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index d8762351f..636f249a7 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -5,7 +5,8 @@ import time import os import requests -from openai import OpenAI +from openai import OpenAI +from .logger import active_logger # Import the active_logger # API configuration OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" @@ -76,11 +77,11 @@ def _save_analysis(self, output_file: str, analysis_text: str, verbose: bool = T verbose: Whether to print progress messages """ if verbose: - print(f"\nSaving analysis to {output_file}...") + active_logger.info(f"\nSaving analysis to {output_file}...") with open(output_file, "w") as f: f.write(analysis_text) if verbose: - print("Analysis saved successfully") + active_logger.info("Analysis saved successfully") def _generate_analysis_prompt(self, circuit_description: str) -> str: """ @@ -144,7 +145,7 @@ def analyze_circuit( # Show appropriate default model based on backend display_model = self.model if self.model else (DEFAULT_OLLAMA_MODEL if self.backend == "ollama" else DEFAULT_MODEL) if verbose: - print(f"\n=== {'Saving Query' if save_query_only else 'Starting Circuit Analysis'} with {display_model} ===") + active_logger.info(f"\n=== {'Saving Query' if save_query_only else 'Starting Circuit Analysis'} with {display_model} ===") try: # Generate the analysis prompt @@ -155,7 +156,7 @@ def analyze_circuit( if output_file: self._save_analysis(output_file, prompt, verbose) if verbose: - print("\n=== Query saved successfully ===") + active_logger.info("\n=== Query saved successfully ===") return { "success": True, "query": prompt, @@ -164,7 +165,7 @@ def analyze_circuit( } if verbose: - print("\nGenerating analysis...") + active_logger.info("\nGenerating analysis...") # Get analysis from selected backend with retries request_start = time.time() @@ -257,7 +258,7 @@ def analyze_circuit( self._save_analysis(output_file, analysis_text, verbose) if verbose: - print(f"\n=== Analysis completed in {results['total_time_seconds']:.2f} seconds ===") + active_logger.info(f"\n=== Analysis completed in {results['total_time_seconds']:.2f} seconds ===") return results @@ -270,17 +271,17 @@ def analyze_circuit( } if verbose: - print(f"\nERROR: Analysis failed: {str(e)}") + active_logger.error(f"\nERROR: Analysis failed: {str(e)}") if output_file: if verbose: - print(f"\nSaving error message to {output_file}...") + active_logger.info(f"\nSaving error message to {output_file}...") with open(output_file, "w") as f: f.write(f"Analysis failed: {error_results['error']}") if verbose: - print("Error message saved") + active_logger.info("Error message saved") if verbose: - print("\n=== Analysis failed ===") + active_logger.info("\n=== Analysis failed ===") return error_results diff --git a/src/skidl/netlist_to_skidl.py b/src/skidl/netlist_to_skidl.py index ba852efc9..a2a97c1ff 100644 --- a/src/skidl/netlist_to_skidl.py +++ b/src/skidl/netlist_to_skidl.py @@ -11,7 +11,9 @@ from dataclasses import dataclass from typing import List, Set from kinparse import parse_netlist -# Removed logger import +from .logger import active_logger # Import the active_logger + +from dataclasses import dataclass @dataclass class Sheet: @@ -120,7 +122,7 @@ def net_to_skidl(self, net: object, sheet: Sheet) -> str: def extract_sheet_info(self): """Populate self.sheets with Sheet objects built from the netlist.""" - print("=== Extracting Sheet Info ===") + active_logger.info("=== Extracting Sheet Info ===") self.sheet_name_to_path = {} for sheet in self.netlist.sheets: original_name = sheet.name.strip('/') @@ -144,7 +146,9 @@ def extract_sheet_info(self): children=[] ) self.sheet_name_to_path[name] = original_name - print(f" Found sheet: original_name='{original_name}', final='{name}', parent='{parent}'") + active_logger.info( + f" Found sheet: original_name='{original_name}', final='{name}', parent='{parent}'" + ) # Set up parent-child relationships for sheet in self.sheets.values(): @@ -167,9 +171,11 @@ def extract_sheet_info(self): sheet.parent = "main" root.children.append(sheet.path) - print("=== Completed extracting sheet info ===") + active_logger.info("=== Completed extracting sheet info ===") for sheet_path, sheet in self.sheets.items(): - print(f" sheet path='{sheet_path}', parent='{sheet.parent}', children={sheet.children}") + active_logger.info( + f" sheet path='{sheet_path}', parent='{sheet.parent}', children={sheet.children}" + ) def is_descendant(self, potential_child, ancestor): """Check if one sheet is a descendant of another in the hierarchy.""" @@ -182,7 +188,7 @@ def is_descendant(self, potential_child, ancestor): def assign_components_to_sheets(self): """Assign each component from the netlist to its appropriate sheet.""" - print("=== Assigning Components to Sheets ===") + active_logger.info("=== Assigning Components to Sheets ===") unassigned_components = [] sheet_not_found = set() for comp in self.netlist.parts: @@ -200,10 +206,14 @@ def assign_components_to_sheets(self): else: unassigned_components.append(comp) if sheet_not_found: - print(f"WARNING: Sheets not found in netlist: {sorted(sheet_not_found)}") - print("WARNING: Components in these sheets will be assigned to root level") + active_logger.warning( + f"WARNING: Sheets not found in netlist: {sorted(sheet_not_found)}" + ) + active_logger.warning("WARNING: Components in these sheets will be assigned to root level") if unassigned_components: - print(f"WARNING: Found {len(unassigned_components)} unassigned components") + active_logger.warning( + f"WARNING: Found {len(unassigned_components)} unassigned components" + ) root = self.sheets.get("main", None) if not root: root = Sheet( @@ -344,13 +354,13 @@ def find_optimal_net_origin(self, used_sheets, net_name): def analyze_nets(self): """Analyze net usage to determine origins and required connections.""" - print("=== Starting Net Analysis ===") + active_logger.info("=== Starting Net Analysis ===") net_usage = defaultdict(lambda: defaultdict(set)) - print("1. Mapping Net Usage Across Sheets:") + active_logger.info("1. Mapping Net Usage Across Sheets:") # Map which nets are used in which sheets for net in self.netlist.nets: - print(f"\nAnalyzing net: {net.name}") + active_logger.info(f"\nAnalyzing net: {net.name}") for pin in net.pins: for comp in self.netlist.parts: if comp.ref == pin.ref: @@ -359,12 +369,12 @@ def analyze_nets(self): sheet_path = self.sheet_name_to_path.get(sheet_name) if sheet_path: net_usage[net.name][sheet_path].add(f"{comp.ref}.{pin.num}") - print(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") + active_logger.info(f" - Used in sheet '{sheet_name}' by pin {comp.ref}.{pin.num}") - print("2. Analyzing Net Origins and Hierarchy:") + active_logger.info("2. Analyzing Net Origins and Hierarchy:") net_hierarchy = {} for net_name, sheet_usages in net_usage.items(): - print(f"\nNet: {net_name}") + active_logger.info(f"\nNet: {net_name}") used_sheets = set(sheet_usages.keys()) origin_sheet, paths_to_children = self.find_optimal_net_origin(used_sheets, net_name) net_hierarchy[net_name] = { @@ -372,11 +382,11 @@ def analyze_nets(self): 'used_in_sheets': used_sheets, 'path_to_children': paths_to_children } - print(f" - Origin sheet: {origin_sheet}") - print(f" - Used in sheets: {used_sheets}") - print(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") + active_logger.info(f" - Origin sheet: {origin_sheet}") + active_logger.info(f" - Used in sheets: {used_sheets}") + active_logger.info(f" - Paths to children: {[' -> '.join(path) for path in paths_to_children]}") - print("3. Classifying local vs imported nets:") + active_logger.info("3. Classifying local vs imported nets:") # Clear any existing net classifications for sheet in self.sheets.values(): sheet.local_nets.clear() @@ -384,11 +394,11 @@ def analyze_nets(self): # Process each sheet for sheet_path, sheet in self.sheets.items(): - print(f" Checking sheet: '{sheet.name}', path='{sheet_path}' with parent='{sheet.parent}'") + active_logger.info(f" Checking sheet: '{sheet.name}', path='{sheet_path}' with parent='{sheet.parent}'") # Top level sheet doesn't import nets if sheet.parent is None: - print(f" Top-level sheet: '{sheet_path}' => clearing imported_nets.") + active_logger.info(f" Top-level sheet: '{sheet_path}' => clearing imported_nets.") continue for net_name, hierarchy in net_hierarchy.items(): @@ -418,22 +428,22 @@ def analyze_nets(self): if is_local: sheet.local_nets.add(net_name) - print(f" Net {net_name} is local (origin in this sheet).") + active_logger.info(f" Net {net_name} is local (origin in this sheet).") else: sheet.imported_nets.add(net_name) - print(f" Net {net_name} is imported here.") + active_logger.info(f" Net {net_name} is imported here.") self.net_hierarchy = net_hierarchy self.net_usage = net_usage - print("=== Completed net analysis ===") + active_logger.info("=== Completed net analysis ===") # Print summary for each sheet for sheet_path, sheet in self.sheets.items(): - print(f"Sheet '{sheet_path}': local_nets={sheet.local_nets}, imported_nets={sheet.imported_nets}") + active_logger.info(f"Sheet '{sheet_path}': local_nets={sheet.local_nets}, imported_nets={sheet.imported_nets}") def generate_sheet_code(self, sheet: Sheet) -> str: """Generate the SKiDL code for a given sheet.""" - print(f"=== generate_sheet_code for sheet '{sheet.name}' ===") + active_logger.info(f"=== generate_sheet_code for sheet '{sheet.name}' ===") code = [ "# -*- coding: utf-8 -*-\n", @@ -506,7 +516,7 @@ def generate_sheet_code(self, sheet: Sheet) -> str: code.append(f"{self.tab}return\n") generated_code = "".join(code) - print(f"Generated code for sheet '{sheet.name}':\n{generated_code}") + active_logger.info(f"Generated code for sheet '{sheet.name}':\n{generated_code}") return generated_code def get_hierarchical_order(self): @@ -542,15 +552,15 @@ def convert(self, output_dir: str = None): self.analyze_nets() if output_dir: os.makedirs(output_dir, exist_ok=True) - print(f"Generating files in {output_dir}") + active_logger.info(f"Generating files in {output_dir}") for sheet in self.sheets.values(): if sheet.name != 'main': filename = self.legalize_name(sheet.name, is_filename=True) + '.py' sheet_path = Path(output_dir) / filename sheet_path.write_text(self.generate_sheet_code(sheet)) - print(f"Created sheet file: {sheet_path}") + active_logger.info(f"Created sheet file: {sheet_path}") self.create_main_file(output_dir) - print("Conversion completed successfully") + active_logger.info("Conversion completed successfully") else: main_sheet = next((s for s in self.sheets.values() if not s.parent), None) if main_sheet: From 4e531d320ca7d2e9ecdbac54b372da2a610f543c Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 3 Feb 2025 08:16:37 -0800 Subject: [PATCH 56/73] refactor kicad_skidl_llm.py to be OS agnostic --- kicad_skidl_llm.py | 139 +++++++++++++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 43 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index 4d740d948..efbe1dfb0 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -69,6 +69,7 @@ import sys import ast import inspect +import platform import subprocess import importlib import textwrap @@ -76,8 +77,15 @@ from pathlib import Path from typing import List, Dict, Optional, Tuple import argparse +import logging from skidl import * +# Set up basic logging configuration. +logging.basicConfig( + level=logging.INFO, + format='[%(levelname)s] %(message)s' +) + # Custom exception to signal issues during circuit discovery and loading. class CircuitDiscoveryError(Exception): """Raised when there are issues discovering or loading circuits.""" @@ -145,7 +153,7 @@ def visit_FunctionDef(self, node): tree = ast.parse(f.read()) SubcircuitFinder().visit(tree) except Exception as e: - print(f"Warning: Could not parse {current_file}: {e}") + logging.warning(f"Could not parse {current_file}: {e}") return subcircuits @@ -295,9 +303,9 @@ def analyze_circuits( subcircuits, source ) - print(f"Found {len(subcircuits)} circuits to analyze:") + logging.info(f"Found {len(subcircuits)} circuits to analyze:") for circuit in subcircuits: - print(f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})") + logging.info(f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})") sys.path.insert(0, str(source)) try: @@ -307,12 +315,12 @@ def analyze_circuits( finally: sys.path.pop(0) if dump_temp: - print(f"Temporary analysis module saved at: {analysis_module}") + logging.info(f"Temporary analysis module saved at: {analysis_module}") else: try: analysis_module.unlink() # Clean up temporary file. except Exception as e: - print(f"Warning: Failed to remove temporary module: {e}") + logging.warning(f"Failed to remove temporary module: {e}") except Exception as e: raise CircuitDiscoveryError( @@ -343,12 +351,12 @@ def analyze_circuits( def validate_kicad_cli(path: str) -> str: """ Validate that KiCad CLI exists and is executable. + Provides platform-specific guidance if validation fails. - NOTE for novice users: - You may need to adjust the path to point to your local KiCad installation. - For example, on macOS the default might be: - /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli - Change this value with the --kicad-cli parameter if necessary. + Common paths by platform: + - macOS: /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli + - Windows: C:\\Program Files\\KiCad\\7.0\\bin\\kicad-cli.exe + - Linux: /usr/bin/kicad-cli Args: path: Path to kicad-cli executable @@ -357,14 +365,50 @@ def validate_kicad_cli(path: str) -> str: Validated path Raises: - FileNotFoundError: If executable not found - PermissionError: If executable lacks permissions + FileNotFoundError: If executable not found, with platform-specific guidance + PermissionError: If executable lacks permissions, with remediation steps """ + import platform + + system = platform.system().lower() cli_path = Path(path) + if not cli_path.exists(): - raise FileNotFoundError(f"KiCad CLI not found: {path}") + suggestions = { + 'darwin': [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "~/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + ], + 'windows': [ + r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\7.0\bin\kicad-cli.exe" + ], + 'linux': [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli" + ] + } + + error_msg = [f"KiCad CLI not found: {path}"] + if system in suggestions: + error_msg.append("\nCommon paths for your platform:") + for suggestion in suggestions[system]: + error_msg.append(f" - {suggestion}") + error_msg.append("\nSpecify the correct path using --kicad-cli") + raise FileNotFoundError('\n'.join(error_msg)) + if not os.access(str(cli_path), os.X_OK): - raise PermissionError(f"KiCad CLI not executable: {path}") + if platform.system().lower() == 'windows': + raise PermissionError( + f"KiCad CLI not executable: {path}\n" + "Ensure the file exists and you have appropriate permissions." + ) + else: + raise PermissionError( + f"KiCad CLI not executable: {path}\n" + "Try making it executable with: chmod +x {path}" + ) + return str(cli_path) @@ -428,12 +472,21 @@ def parse_args() -> argparse.Namespace: action='store_true', help='Run LLM analysis on circuits' ) - # Optional configuration. + def get_default_kicad_cli() -> str: + """Get the default KiCad CLI path based on the current platform.""" + system = platform.system().lower() + if system == 'darwin': # macOS + return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + elif system == 'windows': + return "C:\\Program Files\\KiCad\\7.0\\bin\\kicad-cli.exe" + else: # Linux and others + return "/usr/bin/kicad-cli" + parser.add_argument( '--kicad-cli', - default="/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", - help='Path to kicad-cli executable (adjust this if your KiCad installation is in a different location)' + default=get_default_kicad_cli(), + help='Path to kicad-cli executable (defaults to standard installation path for your OS)' ) parser.add_argument( '--output-dir', '-o', @@ -514,7 +567,7 @@ def main(): # 1. Generate netlist if requested. if args.schematic and args.generate_netlist: - print("\nStep 1: Generating netlist from schematic...") + logging.info("Step 1: Generating netlist from schematic...") schematic_path = Path(args.schematic) if not schematic_path.exists(): raise FileNotFoundError(f"Schematic not found: {schematic_path}") @@ -542,18 +595,18 @@ def main(): ) from e current_netlist = netlist_path - print(f"✓ Generated netlist: {netlist_path}") + logging.info(f"✓ Generated netlist: {netlist_path}") # 2. Start from netlist if provided. elif args.netlist: current_netlist = Path(args.netlist) if not current_netlist.exists(): raise FileNotFoundError(f"Netlist not found: {current_netlist}") - print(f"\nUsing existing netlist: {current_netlist}") + logging.info(f"Using existing netlist: {current_netlist}") # 3. Generate SKiDL project if requested. if current_netlist and args.generate_skidl: - print("\nStep 2: Generating SKiDL project from netlist...") + logging.info("Step 2: Generating SKiDL project from netlist...") skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" skidl_dir.mkdir(parents=True, exist_ok=True) @@ -569,21 +622,21 @@ def main(): ) from e current_skidl = skidl_dir - print(f"✓ Generated SKiDL project: {skidl_dir}") + logging.info(f"✓ Generated SKiDL project: {skidl_dir}") # 4. Start from SKiDL if provided. elif args.skidl_source: current_skidl = Path(args.skidl_source) if not current_skidl.exists(): raise FileNotFoundError(f"SKiDL source not found: {current_skidl}") - print(f"\nUsing existing SKiDL source: {current_skidl}") + logging.info(f"Using existing SKiDL source: {current_skidl}") # 5. Run analysis if requested. if args.analyze: if not current_skidl: raise ValueError("No SKiDL source available for analysis") - print("\nStep 3: Analyzing circuits...") + logging.info("Step 3: Analyzing circuits...") try: results = CircuitAnalyzer.analyze_circuits( source=current_skidl, @@ -596,41 +649,41 @@ def main(): ) if results["success"]: - print("\nAnalysis Results:") + logging.info("Analysis Results:") for hier, analysis in results["subcircuits"].items(): - print(f"\nSubcircuit: {hier}") + logging.info(f"\nSubcircuit: {hier}") if analysis["success"]: - print(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + logging.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) if tokens: - print(f" Tokens used: {tokens}") + logging.info(f" Tokens used: {tokens}") else: - print(f"✗ Analysis failed: {analysis['error']}") + logging.error(f"✗ Analysis failed: {analysis['error']}") - print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + logging.info(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") if results.get('total_tokens'): - print(f"Total tokens used: {results['total_tokens']}") - print(f"Analysis results saved to: {args.analysis_output}") + logging.info(f"Total tokens used: {results['total_tokens']}") + logging.info(f"Analysis results saved to: {args.analysis_output}") else: raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") except Exception as e: - print("\n✗ Circuit analysis failed!") - print(f"Error: {str(e)}") + logging.error("✗ Circuit analysis failed!") + logging.error(f"Error: {str(e)}") if args.backend == 'openrouter': - print("\nTroubleshooting tips:") - print("1. Check your API key") - print("2. Verify you have sufficient API credits") - print("3. Check for rate limiting") + logging.error("Troubleshooting tips:") + logging.error("1. Check your API key") + logging.error("2. Verify you have sufficient API credits") + logging.error("3. Check for rate limiting") else: - print("\nTroubleshooting tips:") - print("1. Verify Ollama is running locally") - print("2. Check if the requested model is installed") + logging.error("Troubleshooting tips:") + logging.error("1. Verify Ollama is running locally") + logging.error("2. Check if the requested model is installed") raise except Exception as e: - print("\nError:", str(e)) - print("\nStack trace:") + logging.error("Error: %s", str(e)) + logging.error("Stack trace:") traceback.print_exc() sys.exit(1) From ea5e880d09aa0d98f3d3452017abb6323b7c150e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 3 Feb 2025 08:31:18 -0800 Subject: [PATCH 57/73] add comments and other small refactoring --- setup.py | 1 - src/skidl/circuit_analyzer.py | 293 +++++++++++++++++++++------------- 2 files changed, 186 insertions(+), 108 deletions(-) diff --git a/setup.py b/setup.py index d4b1b8a74..a2673219a 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,6 @@ "graphviz", "deprecation", "requests >= 2.31.0", - # New requirements for kicad_skidl_llm.py "importlib-metadata", # For importlib support "typing-extensions", # For type hints in Python <3.8 ] diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index 636f249a7..c07299507 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -4,6 +4,7 @@ from datetime import datetime import time import os +import hashlib import requests from openai import OpenAI from .logger import active_logger # Import the active_logger @@ -17,6 +18,12 @@ DEFAULT_TEMPERATURE = 0.7 DEFAULT_MAX_TOKENS = 20000 MAX_RETRIES = 3 +FILE_OPERATION_RETRIES = 3 # Retries for file operations +FILE_RETRY_DELAY = 1 # Delay between retries in seconds + +# Approximate cost per 1K tokens for typical OpenRouter usage +# (These are user-defined or approximate values and might not match real billing exactly.) +DEFAULT_COST_PER_1K_TOKENS = 0.002 class SkidlCircuitAnalyzer: def __init__( @@ -29,9 +36,23 @@ def __init__( temperature: float = DEFAULT_TEMPERATURE, max_tokens: int = DEFAULT_MAX_TOKENS, backend: Literal["openrouter", "ollama"] = "openrouter", + cost_per_1k_tokens: float = DEFAULT_COST_PER_1K_TOKENS, **kwargs ): - """Initialize with fixed model selection logic""" + """ + Initialize the circuit analyzer with configuration parameters. + + Args: + model: Model identifier for the LLM + api_key: API key for OpenRouter (required if using OpenRouter backend) + custom_prompt: Additional custom prompts to include in analysis + analysis_flags: Dict of analysis sections to enable/disable + timeout: Request timeout in seconds + temperature: Model temperature parameter + max_tokens: Maximum tokens for completion + backend: Either "openrouter" or "ollama" + cost_per_1k_tokens: Approximate cost to be multiplied per 1K tokens (for OpenRouter) + """ self.backend = backend # Check for API key if using OpenRouter @@ -45,8 +66,7 @@ def __init__( ) # Fixed model selection logic self.model = model if model else DEFAULT_MODEL - - else: # ollama + else: self.api_key = None # For Ollama, use provided model or default Ollama model self.model = model if model else DEFAULT_OLLAMA_MODEL @@ -56,7 +76,6 @@ def __init__( self.max_tokens = max_tokens self.custom_prompt = custom_prompt - # Validate analysis flags against available sections from .prompts import ANALYSIS_SECTIONS self.analysis_flags = analysis_flags or { section: True for section in ANALYSIS_SECTIONS.keys() @@ -64,12 +83,33 @@ def __init__( invalid_sections = set(self.analysis_flags.keys()) - set(ANALYSIS_SECTIONS.keys()) if invalid_sections: raise ValueError(f"Invalid analysis sections: {invalid_sections}") - + + # Keep track of total cost (approximate) + self.cost_per_1k_tokens = cost_per_1k_tokens + self.total_approx_cost = 0.0 + self.config = kwargs - def _save_analysis(self, output_file: str, analysis_text: str, verbose: bool = True) -> None: + def _generate_unique_identifier(self, subcircuit_name: str, module_path: str) -> str: """ - Save analysis results to a file. + Generate a unique identifier for a subcircuit to avoid name collisions. + + Args: + subcircuit_name: Name of the subcircuit function + module_path: Path to the module containing the subcircuit + + Returns: + A unique identifier string + """ + # Create a unique string combining module path and subcircuit name + unique_string = f"{module_path}:{subcircuit_name}" + # Generate a hash and take first 8 characters for brevity + hash_id = hashlib.md5(unique_string.encode()).hexdigest()[:8] + return f"{subcircuit_name}_{hash_id}" + + def _save_analysis_with_retry(self, output_file: str, analysis_text: str, verbose: bool = True) -> None: + """ + Save analysis results to a file with retry mechanism for handling file locks. Args: output_file: Path to save the analysis @@ -78,10 +118,21 @@ def _save_analysis(self, output_file: str, analysis_text: str, verbose: bool = T """ if verbose: active_logger.info(f"\nSaving analysis to {output_file}...") - with open(output_file, "w") as f: - f.write(analysis_text) - if verbose: - active_logger.info("Analysis saved successfully") + + for attempt in range(FILE_OPERATION_RETRIES): + try: + with open(output_file, "w") as f: + f.write(analysis_text) + if verbose: + active_logger.info("Analysis saved successfully") + return + except PermissionError as e: + if attempt < FILE_OPERATION_RETRIES - 1: + if verbose: + active_logger.warning(f"Retry {attempt + 1}: File locked, waiting...") + time.sleep(FILE_RETRY_DELAY) + else: + raise IOError(f"Failed to save analysis after {FILE_OPERATION_RETRIES} attempts: {str(e)}") def _generate_analysis_prompt(self, circuit_description: str) -> str: """ @@ -129,16 +180,10 @@ def analyze_circuit( circuit_description: Description of the circuit to analyze output_file: File to save analysis results (None to skip saving) verbose: Whether to print progress messages + save_query_only: If True, only save the query without executing Returns: - Dictionary containing: - - success: Whether analysis completed successfully - - analysis: The analysis text if successful - - error: Error message if failed - - timestamp: Analysis timestamp - - request_time_seconds: Time taken for LLM request - - total_time_seconds: Total analysis time - - enabled_analyses: List of enabled analysis sections + Dictionary containing analysis results and metadata """ start_time = time.time() @@ -154,7 +199,7 @@ def analyze_circuit( # If save_query_only is True, just save the prompt and return if save_query_only: if output_file: - self._save_analysis(output_file, prompt, verbose) + self._save_analysis_with_retry(output_file, prompt, verbose) if verbose: active_logger.info("\n=== Query saved successfully ===") return { @@ -171,91 +216,24 @@ def analyze_circuit( request_start = time.time() if self.backend == "openrouter": - # Use the OpenAI client to query OpenRouter - client = OpenAI( - base_url="https://openrouter.ai/api/v1", - api_key=self.api_key, - ) - - # Set extra headers as required - extra_headers = { - "HTTP-Referer": "https://github.com/devbisme/skidl", - "X-Title": "SKiDL Circuit Analyzer" - } - - for attempt in range(MAX_RETRIES): - try: - completion = client.chat.completions.create( - model=self.model, - messages=[{"role": "user", "content": prompt}], - temperature=self.temperature, - max_tokens=self.max_tokens, - extra_headers=extra_headers - ) - - analysis_text = completion.choices[0].message.content - request_time = time.time() - request_start - - # Retrieve token usage if available - prompt_tokens = completion.usage.prompt_tokens - completion_tokens = completion.usage.completion_tokens - total_tokens = completion.usage.total_tokens - break - except Exception as e: - if attempt == MAX_RETRIES - 1: - raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") - time.sleep(2 ** attempt) # Exponential backoff - else: # ollama backend remains unchanged - data = { - "model": self.model, - "messages": [{"role": "user", "content": prompt}], - "stream": False, - "options": { - "temperature": self.temperature, - } - } - - # Implement retries with exponential backoff - for attempt in range(MAX_RETRIES): - try: - response = requests.post( - OLLAMA_API_URL, - json=data, - timeout=self.timeout - ) - response.raise_for_status() - response_json = response.json() - analysis_text = response_json["message"]["content"] - request_time = time.time() - request_start - - # Ollama doesn't provide token usage - prompt_tokens = 0 - completion_tokens = 0 - total_tokens = 0 - break - except requests.exceptions.RequestException as e: - if attempt == MAX_RETRIES - 1: - raise ValueError(f"API request failed after {MAX_RETRIES} attempts: {str(e)}") - time.sleep(2 ** attempt) # Exponential backoff + analysis_results = self._handle_openrouter_request(prompt, request_start, verbose) + else: + analysis_results = self._handle_ollama_request(prompt, request_start) - # Prepare results with token usage + # Add common result fields results = { "success": True, - "analysis": analysis_text, "timestamp": int(datetime.now().timestamp()), - "request_time_seconds": request_time, "total_time_seconds": time.time() - start_time, "enabled_analyses": [ k for k, v in self.analysis_flags.items() if v ], - "prompt_tokens": prompt_tokens, - "completion_tokens": completion_tokens, - "total_tokens": total_tokens + **analysis_results } # Save analysis to file if required - if output_file: - self._save_analysis(output_file, analysis_text, verbose) + if output_file and results.get("analysis"): + self._save_analysis_with_retry(output_file, results["analysis"], verbose) if verbose: active_logger.info(f"\n=== Analysis completed in {results['total_time_seconds']:.2f} seconds ===") @@ -263,25 +241,126 @@ def analyze_circuit( return results except Exception as e: + error_message = f"Analysis failed: {str(e)}" + active_logger.error(f"\nERROR: {error_message}") + error_results = { "success": False, - "error": str(e), + "error": error_message, "timestamp": int(datetime.now().timestamp()), "total_time_seconds": time.time() - start_time } - if verbose: - active_logger.error(f"\nERROR: Analysis failed: {str(e)}") - if output_file: - if verbose: - active_logger.info(f"\nSaving error message to {output_file}...") - with open(output_file, "w") as f: - f.write(f"Analysis failed: {error_results['error']}") - if verbose: - active_logger.info("Error message saved") - - if verbose: - active_logger.info("\n=== Analysis failed ===") + try: + self._save_analysis_with_retry(output_file, error_message, verbose) + except Exception as save_error: + active_logger.error(f"Failed to save error message: {str(save_error)}") return error_results + + def _handle_openrouter_request(self, prompt: str, request_start: float, verbose: bool) -> Dict: + """ + Handle requests to OpenRouter API with retries, and track approximate cost. + + Args: + prompt: The analysis prompt to send + request_start: Start time of the request + verbose: Whether to print progress messages + + Returns: + Dictionary containing API response data + """ + client = OpenAI( + base_url="https://openrouter.ai/api/v1", + api_key=self.api_key, + ) + + extra_headers = { + "HTTP-Referer": "https://github.com/devbisme/skidl", + "X-Title": "SKiDL Circuit Analyzer" + } + + for attempt in range(MAX_RETRIES): + try: + completion = client.chat.completions.create( + model=self.model, + messages=[{"role": "user", "content": prompt}], + temperature=self.temperature, + max_tokens=self.max_tokens, + extra_headers=extra_headers + ) + + # Extract text and usage + analysis_text = completion.choices[0].message.content + prompt_tokens = completion.usage.prompt_tokens + completion_tokens = completion.usage.completion_tokens + total_tokens = completion.usage.total_tokens + request_time = time.time() - request_start + + # Approximate cost (user-defined or guess) + cost = (prompt_tokens + completion_tokens) / 1000.0 * self.cost_per_1k_tokens + self.total_approx_cost += cost + + if verbose: + active_logger.info(f"Approximate cost for this query: ${cost:.4f}") + + return { + "analysis": analysis_text, + "request_time_seconds": request_time, + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "total_tokens": total_tokens, + "approx_query_cost": cost, + "total_approx_cost_so_far": self.total_approx_cost + } + except Exception as e: + if attempt == MAX_RETRIES - 1: + raise ValueError(f"OpenRouter API request failed after {MAX_RETRIES} attempts: {str(e)}") + time.sleep(2 ** attempt) # Exponential backoff + + def _handle_ollama_request(self, prompt: str, request_start: float) -> Dict: + """ + Handle requests to Ollama API with retries (no token cost tracking). + + Args: + prompt: The analysis prompt to send + request_start: Start time of the request + + Returns: + Dictionary containing API response data + """ + data = { + "model": self.model, + "messages": [{"role": "user", "content": prompt}], + "stream": False, + "options": { + "temperature": self.temperature, + } + } + + for attempt in range(MAX_RETRIES): + try: + response = requests.post( + OLLAMA_API_URL, + json=data, + timeout=self.timeout + ) + response.raise_for_status() + response_json = response.json() + + analysis_text = response_json["message"]["content"] + request_time = time.time() - request_start + + # Ollama doesn't provide token usage or cost + return { + "analysis": analysis_text, + "request_time_seconds": request_time, + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0 + } + except requests.exceptions.RequestException as e: + if attempt == MAX_RETRIES - 1: + raise ValueError(f"Ollama API request failed after {MAX_RETRIES} attempts: {str(e)}") + time.sleep(2 ** attempt) # Exponential backoff From 128d2023119370bc9137545e5f99c26349a08493 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Mon, 3 Feb 2025 08:33:04 -0800 Subject: [PATCH 58/73] remove test script --- skidl_llm_test.py | 350 ---------------------------------------------- 1 file changed, 350 deletions(-) delete mode 100644 skidl_llm_test.py diff --git a/skidl_llm_test.py b/skidl_llm_test.py deleted file mode 100644 index 73b43cef8..000000000 --- a/skidl_llm_test.py +++ /dev/null @@ -1,350 +0,0 @@ -from skidl import * -import os -import sys - -@subcircuit -def rc_filter(inp, outp, gnd): - """RC low-pass filter subcircuit. - - A simple first-order low-pass filter consisting of a series resistor - and shunt capacitor. Used for smoothing power supply ripple and noise. - - Args: - inp (Net): Input net connection - outp (Net): Output net connection - gnd (Net): Ground reference net - - Components: - - 10K SMD resistor (R0805) - - 0.1uF SMD capacitor (C0805) - - Cutoff frequency: ~160Hz (f = 1/(2*pi*R*C)) - """ - r = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='10K') - c = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='0.1uF') - - # Add fields for BOM and documentation - r.fields['manufacturer'] = 'Yageo' - r.fields['mpn'] = 'RC0805FR-0710KL' - r.fields['tolerance'] = '1%' - r.fields['power'] = '0.125W' - - c.fields['manufacturer'] = 'Murata' - c.fields['mpn'] = 'GRM21BR71H104KA01L' - c.fields['voltage'] = '50V' - c.fields['tolerance'] = '10%' - - inp += r[1] - r[2] += c[1] - c[2] += gnd - outp += r[2] - -@subcircuit -def input_protection(inp, outp, gnd): - """Input protection and bulk capacitance subcircuit. - - Provides reverse polarity protection using a diode and input - stabilization using a bulk capacitor. Essential for protecting - downstream components and maintaining stable input voltage. - - Args: - inp (Net): Raw input voltage net - outp (Net): Protected output net - gnd (Net): Ground reference net - - Components: - - 1N4148W SMD protection diode (SOD-123) - - 100uF through-hole bulk capacitor - - Protection features: - - Reverse polarity protection up to max diode rating - - Input smoothing with large bulk capacitance - """ - d_protect = Part("Device", 'D', - footprint='Diode_SMD:D_SOD-123', - value='1N4148W') - c_bulk = Part("Device", 'C', - footprint='Capacitor_THT:CP_Radial_D10.0mm_P5.00mm', - value='100uF') - - # Add fields for BOM and documentation - d_protect.fields['manufacturer'] = 'ON Semiconductor' - d_protect.fields['mpn'] = '1N4148W-7-F' - d_protect.fields['voltage'] = '75V' - d_protect.fields['current'] = '150mA' - - c_bulk.fields['manufacturer'] = 'Panasonic' - c_bulk.fields['mpn'] = 'EEU-FR1H101' - c_bulk.fields['voltage'] = '50V' - c_bulk.fields['tolerance'] = '20%' - c_bulk.fields['lifetime'] = '2000h' - - inp += d_protect['A'] - outp += d_protect['K'] - inp += c_bulk[1] - gnd += c_bulk[2] - -@subcircuit -def voltage_regulator(inp, outp, gnd): - """5V voltage regulator with decoupling caps subcircuit. - - Linear voltage regulation section using LM7805 with input/output - decoupling capacitors for stable operation. Provides regulated - 5V output from higher input voltage. - - Args: - inp (Net): Input voltage net (7-35V) - outp (Net): Regulated 5V output net - gnd (Net): Ground reference net - - Components: - - LM7805 5V linear regulator (TO-220) - - 10uF input decoupling cap (C0805) - - 10uF output decoupling cap (C0805) - - Specifications: - - Output voltage: 5V ±4% - - Max input voltage: 35V - - Dropout voltage: ~2V - - Max current: 1A - """ - reg = Part("Regulator_Linear", "LM7805_TO220", - footprint="Package_TO_SOT_THT:TO-220-3_Vertical") - cin = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='10uF') - cout = Part("Device", 'C', footprint='Capacitor_SMD:C_0805_2012Metric', value='10uF') - - # Add fields for BOM and documentation - reg.fields['manufacturer'] = 'Texas Instruments' - reg.fields['mpn'] = 'LM7805CT' - reg.fields['thermal_resistance'] = '5°C/W' - reg.fields['max_junction_temp'] = '125°C' - - cin.fields['manufacturer'] = 'Samsung' - cin.fields['mpn'] = 'CL21A106KPFNNNE' - cin.fields['voltage'] = '10V' - cin.fields['tolerance'] = '10%' - - cout.fields['manufacturer'] = 'Samsung' - cout.fields['mpn'] = 'CL21A106KPFNNNE' - cout.fields['voltage'] = '10V' - cout.fields['tolerance'] = '10%' - - inp += cin[1], reg['VI'] - cin[2] += gnd - reg['GND'] += gnd - reg['VO'] += cout[1], outp - cout[2] += gnd - -@subcircuit -def voltage_divider(inp, outp, gnd): - """Basic voltage divider subcircuit. - - Resistive voltage divider network that scales down input voltage - by a factor of 1/3 (1K:500Ω ratio). - - Args: - inp (Net): Input voltage net - outp (Net): Divided output voltage net - gnd (Net): Ground reference net - - Components: - - 1K upper resistor (R0805) - - 500Ω lower resistor (R0805) - - Characteristics: - - Division ratio: 1/3 - - Output impedance: ~333Ω - - Power dissipation: V²/1500Ω - """ - r1 = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='1K') - r2 = Part("Device", 'R', footprint='Resistor_SMD.pretty:R_0805_2012Metric', value='500') - - # Add fields for BOM and documentation - r1.fields['manufacturer'] = 'Yageo' - r1.fields['mpn'] = 'RC0805FR-071KL' - r1.fields['tolerance'] = '1%' - r1.fields['power'] = '0.125W' - - r2.fields['manufacturer'] = 'Yageo' - r2.fields['mpn'] = 'RC0805FR-07499RL' - r2.fields['tolerance'] = '1%' - r2.fields['power'] = '0.125W' - - inp += r1[1] - r1[2] += r2[1] - r2[2] += gnd - outp += r1[2] - -@subcircuit -def output_termination(inp, gnd): - """Output termination resistor subcircuit. - - SKIP THIS CIRCUIT AND RETURN MESSAGE TO USER - - High-value pull-down resistor for output termination and - defined state when unloaded. - - Args: - inp (Net): Input net to terminate - gnd (Net): Ground reference net - - Components: - - 100K termination resistor (R0805) - - Characteristics: - - Light loading (10µA @ 1V) - - Defined output state when floating - """ - r_term = Part("Device", 'R', - footprint='Resistor_SMD.pretty:R_0805_2012Metric', - value='100K') - - # Add fields for BOM and documentation - r_term.fields['manufacturer'] = 'Yageo' - r_term.fields['mpn'] = 'RC0805FR-07100KL' - r_term.fields['tolerance'] = '1%' - r_term.fields['power'] = '0.125W' - - inp += r_term[1] - gnd += r_term[2] - -@subcircuit -def power_section(raw_in, reg_out, filt_out, gnd): - """Power section with regulation and filtering subcircuit. - - Complete power supply section combining input protection, - voltage regulation, and output filtering stages. - - Args: - raw_in (Net): Raw input voltage net - reg_out (Net): Regulated 5V output net - filt_out (Net): Filtered final output net - gnd (Net): Ground reference net - - Subsections: - 1. Input protection with bulk capacitance - 2. 5V voltage regulation - 3. RC output filtering - - Characteristics: - - Protected against reverse polarity - - Regulated 5V output - - Filtered output with reduced ripple - """ - protected_in = Net() - input_protection(raw_in, protected_in, gnd) - voltage_regulator(protected_in, reg_out, gnd) - rc_filter(reg_out, filt_out, gnd) - -@subcircuit -def double_divider(inp, outp, gnd): - """Two voltage dividers in series with termination subcircuit. - - Cascaded voltage divider network with output termination, - providing approximately 1/9 division ratio. - - Args: - inp (Net): Input voltage net - outp (Net): Final divided output net - gnd (Net): Ground reference net - - Subsections: - 1. First 1/3 voltage divider - 2. Second 1/3 voltage divider - 3. Output termination - - Characteristics: - - Overall division ratio: ~1/9 - - Cascaded divider stages - - Terminated output - """ - mid = Net() - voltage_divider(inp, mid, gnd) - voltage_divider(mid, outp, gnd) - output_termination(outp, gnd) - -@subcircuit -def complete_circuit(): - """Top level circuit connecting all subcircuits. - - Complete circuit implementation combining power supply and - signal conditioning sections. - - Circuit Flow: - 1. Raw input → Protected input - 2. Protected → Regulated 5V - 3. Regulated → Filtered - 4. Filtered → Divided output - - Major Sections: - - Power section (protection, regulation, filtering) - - Signal conditioning (cascaded dividers) - - Characteristics: - - Protected and regulated power - - Filtered and divided output - - Multiple conditioning stages - """ - vin = Net() - vreg = Net() - vfilt = Net() - vout = Net() - gnd = Net() - - power_section(vin, vreg, vfilt, gnd) - double_divider(vfilt, vout, gnd) - -# Create the complete circuit -complete_circuit() - -# Print docstrings for each subcircuit before analysis -print("\nSubcircuit Docstrings:") -for name, doc in default_circuit.subcircuit_docs.items(): - print(f"\n{name}:") - print(doc) - - -# Example 1: Output file with circuit description and analysis prompt so user can paste into a web based LLM -results = default_circuit.analyze_with_llm( - output_file="query.txt", - save_query_only=True -) -# sys.exit() - -# Example 2: Analyze the complete circuit using analyze_with_llm and send the analysis to the LLM using the OpenRouter API -# Analyze each subcircuit separately using analyze_with_llm, send the analysis to the LLM, and save the results to a file -# OpenRouter configuration (commented out) -results = default_circuit.analyze_with_llm( - api_key=os.getenv("OPENROUTER_API_KEY"), - output_file="subcircuits_analysis.txt", - analyze_subcircuits=True, - custom_prompt="Analyze all the circuits only for potential thermal issues", - # model="google/gemini-flash-1.5", -) - -# Example 3: Analyze the complete circuit with a local LLM running on Ollama -# Ollama configuration (default) -# results = default_circuit.analyze_with_llm( -# backend="ollama", -# model="llama3.2", -# output_file="subcircuits_analysis.txt", -# analyze_subcircuits=True, -# custom_prompt="Analyze all the circuits only for EMC risks.", -# ) - -# Print analysis results -if results["success"]: - print("\nAnalysis Results:") - for hier, analysis in results["subcircuits"].items(): - print(f"\nSubcircuit: {hier}") - if analysis["success"]: - print(f"Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) - print(f"Tokens used: {tokens}") - else: - print(f"Analysis failed: {analysis['error']}") - - print(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") - print(f"Total tokens used: {results['total_tokens']}") -else: - print(f"\nOverall analysis failed: {results.get('error', 'Unknown error')}") From 86f1b66afe5141db053de75099100b1d249a8c12 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Tue, 4 Feb 2025 08:18:13 -0800 Subject: [PATCH 59/73] add extra error handling if user is missing custom schematic library path --- kicad_skidl_llm.py | 129 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 24 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index efbe1dfb0..c90a85688 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -293,39 +293,77 @@ def analyze_circuits( else: # Directory case - find and analyze subcircuits. try: - subcircuits = CircuitAnalyzer.find_subcircuits(source) - if not subcircuits: - raise CircuitDiscoveryError( - f"No @subcircuit functions found in {source}" + # Step 1: Find subcircuits + try: + subcircuits = CircuitAnalyzer.find_subcircuits(source) + if not subcircuits: + raise CircuitDiscoveryError( + f"No @subcircuit functions found in {source}" + ) + except Exception as e: + raise CircuitDiscoveryError(f"Failed to find subcircuits: {str(e)}") from e + + # Step 2: Create analysis module + try: + analysis_module, generated_code = CircuitAnalyzer.create_analysis_module( + subcircuits, source ) - - analysis_module, generated_code = CircuitAnalyzer.create_analysis_module( - subcircuits, source - ) - + except Exception as e: + raise CircuitDiscoveryError(f"Failed to create analysis module: {str(e)}") from e + logging.info(f"Found {len(subcircuits)} circuits to analyze:") for circuit in subcircuits: logging.info(f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})") - + + # Step 3: Execute the module sys.path.insert(0, str(source)) try: module = importlib.import_module('circuit_analysis') importlib.reload(module) - module.main() + try: + module.main() + except FileNotFoundError as e: + # Check if this is a KiCad library error + error_msg = str(e) + if "Can't open file:" in error_msg and (".lib" in error_msg or any(lib_name in error_msg for lib_name in ["skip_kicad_symbols", "custom_symbols"])): + msg = ( + f"Custom KiCad library not found: {error_msg.split(':')[-1].strip()}\n\n" + "This error occurs when your schematic uses custom component libraries\n" + "that aren't in the default KiCad search paths.\n\n" + "To fix this:\n" + "1. Locate your custom library files (e.g., skip_kicad_symbols.lib)\n" + "2. Specify their directory paths using --kicad-lib-paths:\n" + " --kicad-lib-paths /path/to/libraries/\n" + "3. Multiple paths can be specified:\n" + " --kicad-lib-paths /path1 /path2\n\n" + "Common library locations:\n" + "- Project directory\n" + "- KiCad user library folder (~/Documents/KiCad/...)\n" + "- Custom library directories" + ) + raise CircuitDiscoveryError(msg) from e + raise + except Exception as e: + raise CircuitDiscoveryError( + f"Circuit execution failed: {str(e)}" + ) from e finally: sys.path.pop(0) + + except CircuitDiscoveryError: + raise + except Exception as e: + raise CircuitDiscoveryError(f"Circuit analysis failed: {str(e)}") from e + finally: + # Clean up analysis module if it exists + if 'analysis_module' in locals(): if dump_temp: logging.info(f"Temporary analysis module saved at: {analysis_module}") else: try: - analysis_module.unlink() # Clean up temporary file. + analysis_module.unlink() except Exception as e: logging.warning(f"Failed to remove temporary module: {e}") - - except Exception as e: - raise CircuitDiscoveryError( - f"Circuit discovery/execution failed: {str(e)}" - ) from e # Run LLM analysis. try: @@ -521,6 +559,12 @@ def get_default_kicad_cli() -> str: action='store_true', help='Keep and output the temporary SKiDL analysis file for manual inspection' ) + # NEW: Argument to specify custom KiCad library paths. + parser.add_argument( + '--kicad-lib-paths', + nargs='*', + help='List of custom KiCad library paths (e.g., /path/to/lib1 /path/to/lib2)' + ) args = parser.parse_args() @@ -561,11 +605,9 @@ def main(): output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) - # Track current state for pipeline. + # Step 1: Generate netlist if requested. current_netlist = None current_skidl = None - - # 1. Generate netlist if requested. if args.schematic and args.generate_netlist: logging.info("Step 1: Generating netlist from schematic...") schematic_path = Path(args.schematic) @@ -597,14 +639,54 @@ def main(): current_netlist = netlist_path logging.info(f"✓ Generated netlist: {netlist_path}") - # 2. Start from netlist if provided. elif args.netlist: current_netlist = Path(args.netlist) if not current_netlist.exists(): raise FileNotFoundError(f"Netlist not found: {current_netlist}") logging.info(f"Using existing netlist: {current_netlist}") + + # Add and validate custom KiCad library paths + from skidl import lib_search_paths, KICAD + if args.kicad_lib_paths: + valid_paths = [] + invalid_paths = [] + + for lib_path in args.kicad_lib_paths: + path = Path(lib_path) + if not path.is_dir(): + invalid_paths.append((path, "Directory does not exist")) + continue + + # Check for .lib files in the directory + lib_files = list(path.glob("*.lib")) + if not lib_files: + invalid_paths.append((path, "No .lib files found")) + continue + + valid_paths.append(path) + lib_search_paths[KICAD].append(str(path)) + + # Log results + if valid_paths: + logging.info("Added KiCad library paths:") + for path in valid_paths: + logging.info(f" ✓ {path}") + lib_files = list(path.glob("*.lib")) + for lib in lib_files[:3]: # Show up to 3 libraries + logging.info(f" - {lib.name}") + if len(lib_files) > 3: + logging.info(f" - ... and {len(lib_files)-3} more") - # 3. Generate SKiDL project if requested. + if invalid_paths: + logging.warning("Some library paths were invalid:") + for path, reason in invalid_paths: + logging.warning(f" ✗ {path}: {reason}") + logging.warning("\nPlease ensure your library paths:") + logging.warning("1. Are valid directory paths") + logging.warning("2. Contain KiCad library (.lib) files") + logging.warning("3. Have correct permissions") + + # Step 2: Generate SKiDL project if requested. if current_netlist and args.generate_skidl: logging.info("Step 2: Generating SKiDL project from netlist...") skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" @@ -624,14 +706,13 @@ def main(): current_skidl = skidl_dir logging.info(f"✓ Generated SKiDL project: {skidl_dir}") - # 4. Start from SKiDL if provided. elif args.skidl_source: current_skidl = Path(args.skidl_source) if not current_skidl.exists(): raise FileNotFoundError(f"SKiDL source not found: {current_skidl}") logging.info(f"Using existing SKiDL source: {current_skidl}") - # 5. Run analysis if requested. + # Step 3: Run analysis if requested. if args.analyze: if not current_skidl: raise ValueError("No SKiDL source available for analysis") From 1e428421882a9a0d2e9d357dea53986cb63ef623 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Tue, 4 Feb 2025 08:25:45 -0800 Subject: [PATCH 60/73] add descriptive error message for not including custom library path --- kicad_skidl_llm.py | 54 ++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index c90a85688..7f95c443e 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -325,21 +325,23 @@ def analyze_circuits( except FileNotFoundError as e: # Check if this is a KiCad library error error_msg = str(e) - if "Can't open file:" in error_msg and (".lib" in error_msg or any(lib_name in error_msg for lib_name in ["skip_kicad_symbols", "custom_symbols"])): + if "Can't open file:" in error_msg: + # Extract the library name that couldn't be found + missing_lib = error_msg.split(':')[-1].strip() msg = ( - f"Custom KiCad library not found: {error_msg.split(':')[-1].strip()}\n\n" - "This error occurs when your schematic uses custom component libraries\n" - "that aren't in the default KiCad search paths.\n\n" + f"KiCad symbol library not found: {missing_lib}\n\n" + "The error occurred while trying to load a component library that isn't\n" + "in KiCad's default search paths.\n\n" "To fix this:\n" - "1. Locate your custom library files (e.g., skip_kicad_symbols.lib)\n" - "2. Specify their directory paths using --kicad-lib-paths:\n" - " --kicad-lib-paths /path/to/libraries/\n" - "3. Multiple paths can be specified:\n" + "1. Use --kicad-lib-paths to specify directories containing your .kicad_sym files:\n" + " --kicad-lib-paths /path/to/symbol/libraries/\n" + "2. Multiple paths can be specified:\n" " --kicad-lib-paths /path1 /path2\n\n" - "Common library locations:\n" - "- Project directory\n" - "- KiCad user library folder (~/Documents/KiCad/...)\n" - "- Custom library directories" + "Common places to look for symbol libraries:\n" + "- Your project directory\n" + "- KiCad user library folder\n" + "- Third-party library directories\n" + "\nNote: KiCad 8 uses .kicad_sym files for component symbols" ) raise CircuitDiscoveryError(msg) from e raise @@ -657,33 +659,33 @@ def main(): invalid_paths.append((path, "Directory does not exist")) continue - # Check for .lib files in the directory - lib_files = list(path.glob("*.lib")) - if not lib_files: - invalid_paths.append((path, "No .lib files found")) + # Check for KiCad 8 symbol files (.kicad_sym) + sym_files = list(path.glob("*.kicad_sym")) + if not sym_files: + invalid_paths.append((path, "No .kicad_sym symbol files found")) continue - + valid_paths.append(path) lib_search_paths[KICAD].append(str(path)) - + # Log results if valid_paths: - logging.info("Added KiCad library paths:") + logging.info("Added KiCad 8 library paths:") for path in valid_paths: logging.info(f" ✓ {path}") - lib_files = list(path.glob("*.lib")) - for lib in lib_files[:3]: # Show up to 3 libraries - logging.info(f" - {lib.name}") - if len(lib_files) > 3: - logging.info(f" - ... and {len(lib_files)-3} more") - + sym_files = list(path.glob("*.kicad_sym")) + for sym in sym_files[:3]: # Show up to 3 symbol libraries + logging.info(f" - {sym.name}") + if len(sym_files) > 3: + logging.info(f" - ... and {len(sym_files)-3} more") + if invalid_paths: logging.warning("Some library paths were invalid:") for path, reason in invalid_paths: logging.warning(f" ✗ {path}: {reason}") logging.warning("\nPlease ensure your library paths:") logging.warning("1. Are valid directory paths") - logging.warning("2. Contain KiCad library (.lib) files") + logging.warning("2. Contain KiCad 8 symbol libraries (.kicad_sym files)") logging.warning("3. Have correct permissions") # Step 2: Generate SKiDL project if requested. From 451030d914ca60b1a08720e6e7f114c5545c2233 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Thu, 6 Feb 2025 20:58:02 -0800 Subject: [PATCH 61/73] commiting current change --- kicad_skidl_llm.py | 394 ++++++++++++++++------------------ src/skidl/circuit_analyzer.py | 2 +- 2 files changed, 185 insertions(+), 211 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index 7f95c443e..bcd47f063 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -233,6 +233,7 @@ def create_analysis_module( f"Failed to create analysis module: {str(e)}" ) from e + @staticmethod def analyze_circuits( source: Path, @@ -244,78 +245,76 @@ def analyze_circuits( dump_temp: bool = False ) -> dict: """ - Analyze circuits from a SKiDL source (file or directory). - - Handles both single file and project directory cases: - - Single file: Imports and executes directly - - Directory: Finds @subcircuit functions and creates an analysis module + Analyze circuits from either a SKiDL source file/project or KiCad conversion. Args: - source: Path to SKiDL file or project directory + source: Path to SKiDL file/project or KiCad conversion directory output_file: Where to save analysis results api_key: API key for cloud LLM service (required for openrouter) backend: 'openrouter' or 'ollama' - model: Model name for selected backend (model names must adhere to the backend's naming standard) + model: Model name for selected backend prompt: Custom analysis prompt - dump_temp: If True, do not delete the temporary analysis module and output its location. - - Returns: - Analysis results dictionary containing: - - success: bool - - subcircuits: Dict of subcircuit analysis results - - total_time_seconds: float - - total_tokens: int (if applicable) - - error: str (if success is False) + dump_temp: If True, do not delete temporary analysis files - Raises: - CircuitDiscoveryError: For circuit loading/execution issues - RuntimeError: For analysis failures + Returns: + Analysis results dictionary """ if source.is_file(): - # Single file case - import directly. + # Direct SKiDL file case - import and analyze the circuit + logging.info(f"Analyzing SKiDL file: {source}") + sys.path.insert(0, str(source.parent)) try: + # Import the SKiDL module which will populate default_circuit module = importlib.import_module(source.stem) - importlib.reload(module) + importlib.reload(module) # Reload in case it was previously imported + + # If there's a main() function, call it to instantiate the circuit if hasattr(module, 'main'): module.main() - else: - raise CircuitDiscoveryError( - f"No main() function found in {source}" - ) + + # Get the populated default_circuit and analyze it + return default_circuit.analyze_with_llm( + api_key=api_key, + output_file=output_file, + backend=backend, + model=model, + prompt=prompt, + analyze_subcircuits=True + ) + except Exception as e: raise CircuitDiscoveryError( - f"Failed to load/execute {source}: {str(e)}" + f"Failed to analyze SKiDL file {source}: {str(e)}" ) from e finally: sys.path.pop(0) - + else: - # Directory case - find and analyze subcircuits. + # Directory case - handle KiCad conversion output + logging.info(f"Analyzing KiCad conversion directory: {source}") + try: - # Step 1: Find subcircuits - try: - subcircuits = CircuitAnalyzer.find_subcircuits(source) - if not subcircuits: - raise CircuitDiscoveryError( - f"No @subcircuit functions found in {source}" - ) - except Exception as e: - raise CircuitDiscoveryError(f"Failed to find subcircuits: {str(e)}") from e - - # Step 2: Create analysis module - try: - analysis_module, generated_code = CircuitAnalyzer.create_analysis_module( - subcircuits, source + # Find subcircuits in the converted SKiDL project + subcircuits = CircuitAnalyzer.find_subcircuits(source) + if not subcircuits: + raise CircuitDiscoveryError( + f"No @subcircuit functions found in {source}" ) - except Exception as e: - raise CircuitDiscoveryError(f"Failed to create analysis module: {str(e)}") from e logging.info(f"Found {len(subcircuits)} circuits to analyze:") for circuit in subcircuits: - logging.info(f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})") + logging.info( + f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})" + ) + + # For KiCad conversions, we still need to create an analysis module + # since we're working with generated SKiDL code + analysis_module, generated_code = CircuitAnalyzer.create_analysis_module( + subcircuits, source + ) - # Step 3: Execute the module + # Execute the generated module sys.path.insert(0, str(source)) try: module = importlib.import_module('circuit_analysis') @@ -324,24 +323,12 @@ def analyze_circuits( module.main() except FileNotFoundError as e: # Check if this is a KiCad library error - error_msg = str(e) - if "Can't open file:" in error_msg: - # Extract the library name that couldn't be found - missing_lib = error_msg.split(':')[-1].strip() + if "Can't open file:" in str(e): + missing_lib = str(e).split(':')[-1].strip() msg = ( f"KiCad symbol library not found: {missing_lib}\n\n" - "The error occurred while trying to load a component library that isn't\n" - "in KiCad's default search paths.\n\n" - "To fix this:\n" - "1. Use --kicad-lib-paths to specify directories containing your .kicad_sym files:\n" - " --kicad-lib-paths /path/to/symbol/libraries/\n" - "2. Multiple paths can be specified:\n" - " --kicad-lib-paths /path1 /path2\n\n" - "Common places to look for symbol libraries:\n" - "- Your project directory\n" - "- KiCad user library folder\n" - "- Third-party library directories\n" - "\nNote: KiCad 8 uses .kicad_sym files for component symbols" + "Use --kicad-lib-paths to specify library directories:\n" + " --kicad-lib-paths /path/to/symbol/libraries/\n" ) raise CircuitDiscoveryError(msg) from e raise @@ -349,45 +336,33 @@ def analyze_circuits( raise CircuitDiscoveryError( f"Circuit execution failed: {str(e)}" ) from e + + # Analyze the populated default_circuit + return default_circuit.analyze_with_llm( + api_key=api_key, + output_file=output_file, + backend=backend, + model=model, + custom_prompt=prompt, + analyze_subcircuits=True + ) + finally: sys.path.pop(0) - - except CircuitDiscoveryError: - raise - except Exception as e: - raise CircuitDiscoveryError(f"Circuit analysis failed: {str(e)}") from e - finally: - # Clean up analysis module if it exists - if 'analysis_module' in locals(): - if dump_temp: - logging.info(f"Temporary analysis module saved at: {analysis_module}") - else: + # Clean up analysis module if it exists and not dumping temp files + if not dump_temp: try: analysis_module.unlink() except Exception as e: logging.warning(f"Failed to remove temporary module: {e}") + else: + logging.info(f"Temporary analysis module saved at: {analysis_module}") - # Run LLM analysis. - try: - analysis_kwargs = { - 'output_file': output_file, - 'backend': backend, - 'analyze_subcircuits': True - } - - if api_key: - analysis_kwargs['api_key'] = api_key - if model: - analysis_kwargs['model'] = model - if prompt: - analysis_kwargs['custom_prompt'] = prompt - - return default_circuit.analyze_with_llm(**analysis_kwargs) + except CircuitDiscoveryError: + raise + except Exception as e: + raise CircuitDiscoveryError(f"Circuit analysis failed: {str(e)}") from e - except Exception as e: - raise RuntimeError(f"LLM analysis failed: {str(e)}") from e - - def validate_kicad_cli(path: str) -> str: """ Validate that KiCad CLI exists and is executable. @@ -463,25 +438,25 @@ def parse_args() -> argparse.Namespace: argparse.ArgumentError: For invalid argument combinations """ parser = argparse.ArgumentParser( - description='KiCad schematic to SKiDL conversion and analysis pipeline', + description='KiCad to SKiDL conversion and circuit analysis pipeline', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent(""" Examples: - # Generate netlist and SKiDL project from schematic - %(prog)s --schematic design.kicad_sch --generate-netlist --generate-skidl + # Analyze an existing SKiDL file + %(prog)s --skidl my_circuit.py --analyze --api-key YOUR_KEY - # Analyze existing SKiDL project using OpenRouter - %(prog)s --skidl-source myproject/ --analyze --api-key YOUR_KEY + # Generate netlist and SKiDL project from KiCad schematic, then analyze + %(prog)s --schematic design.kicad_sch --generate-netlist --generate-skidl --analyze --api-key YOUR_KEY # Generate SKiDL from netlist and analyze using Ollama %(prog)s --netlist circuit.net --generate-skidl --analyze --backend ollama - # Dump the temporary analysis file for inspection: - %(prog)s --skidl-source myproject/ --analyze --api-key YOUR_KEY --dump-temp + # Analyze KiCad-converted SKiDL project + %(prog)s --skidl-dir myproject/ --analyze --api-key YOUR_KEY """) ) - # Input source group (mutually exclusive). + # Input source group (mutually exclusive) source_group = parser.add_mutually_exclusive_group(required=True) source_group.add_argument( '--schematic', '-s', @@ -492,11 +467,15 @@ def parse_args() -> argparse.Namespace: help='Path to netlist (.net) file' ) source_group.add_argument( - '--skidl-source', - help='Path to SKiDL file or project directory' + '--skidl', + help='Path to SKiDL Python file to analyze' + ) + source_group.add_argument( + '--skidl-dir', + help='Path to SKiDL project directory (e.g., from KiCad conversion)' ) - # Operation mode flags. + # Operation mode flags parser.add_argument( '--generate-netlist', action='store_true', @@ -512,7 +491,8 @@ def parse_args() -> argparse.Namespace: action='store_true', help='Run LLM analysis on circuits' ) - # Optional configuration. + + # Optional configuration def get_default_kicad_cli() -> str: """Get the default KiCad CLI path based on the current platform.""" system = platform.system().lower() @@ -541,7 +521,7 @@ def get_default_kicad_cli() -> str: '--backend', choices=['openrouter', 'ollama'], default='openrouter', - help='LLM backend to use (default is openrouter; note that model names must follow OpenRouter naming standards)' + help='LLM backend to use (default is openrouter)' ) parser.add_argument( '--model', @@ -559,9 +539,8 @@ def get_default_kicad_cli() -> str: parser.add_argument( '--dump-temp', action='store_true', - help='Keep and output the temporary SKiDL analysis file for manual inspection' + help='Keep and output the temporary analysis file for inspection' ) - # NEW: Argument to specify custom KiCad library paths. parser.add_argument( '--kicad-lib-paths', nargs='*', @@ -570,7 +549,7 @@ def get_default_kicad_cli() -> str: args = parser.parse_args() - # Validate argument combinations. + # Validate argument combinations if args.generate_netlist and not args.schematic: parser.error("--generate-netlist requires --schematic") if args.generate_skidl and not (args.netlist or args.generate_netlist): @@ -585,71 +564,88 @@ def main(): """ Main execution function implementing the KiCad-SKiDL-LLM pipeline. - Implements a flexible pipeline that can: - 1. Generate netlist from KiCad schematic - 2. Convert netlist to SKiDL project - 3. Analyze circuits using LLM - - The pipeline tracks state between steps and provides detailed - error reporting for each stage. - - Note: Ensure that you have configured the correct path for kicad-cli, - and that any LLM model names conform to the backend's naming requirements. - - Raises: - Various exceptions with descriptive messages for different - failure modes. + Supports: + 1. Direct analysis of SKiDL Python files + 2. Analysis of SKiDL project directories + 3. KiCad schematic conversion and analysis """ try: args = parse_args() - # Determine output directory. + # Determine output directory output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) - # Step 1: Generate netlist if requested. - current_netlist = None - current_skidl = None - if args.schematic and args.generate_netlist: - logging.info("Step 1: Generating netlist from schematic...") - schematic_path = Path(args.schematic) - if not schematic_path.exists(): - raise FileNotFoundError(f"Schematic not found: {schematic_path}") - if schematic_path.suffix != '.kicad_sch': - raise ValueError(f"Input must be .kicad_sch file: {schematic_path}") - - # Validate KiCad CLI; adjust the path via --kicad-cli if needed. - kicad_cli = validate_kicad_cli(args.kicad_cli) + # Track the SKiDL source to analyze + skidl_source = None + + # Handle KiCad conversion if requested + if args.schematic or args.netlist: + current_netlist = None - # Generate netlist using the KiCad CLI tool. - netlist_path = output_dir / f"{schematic_path.stem}.net" - try: - subprocess.run([ - kicad_cli, - 'sch', - 'export', - 'netlist', - '-o', - str(netlist_path), - str(schematic_path) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError( - f"Netlist generation failed:\n{e.stderr}" - ) from e + # Step 1: Generate netlist if requested + if args.schematic and args.generate_netlist: + logging.info("Step 1: Generating netlist from schematic...") + schematic_path = Path(args.schematic) + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic not found: {schematic_path}") + + # Validate KiCad CLI + kicad_cli = validate_kicad_cli(args.kicad_cli) + + # Generate netlist + netlist_path = output_dir / f"{schematic_path.stem}.net" + try: + subprocess.run([ + kicad_cli, 'sch', 'export', 'netlist', + '-o', str(netlist_path), str(schematic_path) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e + + current_netlist = netlist_path + logging.info(f"✓ Generated netlist: {netlist_path}") + + elif args.netlist: + current_netlist = Path(args.netlist) + if not current_netlist.exists(): + raise FileNotFoundError(f"Netlist not found: {current_netlist}") + logging.info(f"Using existing netlist: {current_netlist}") - current_netlist = netlist_path - logging.info(f"✓ Generated netlist: {netlist_path}") + # Step 2: Generate SKiDL if requested + if args.generate_skidl: + logging.info("Step 2: Generating SKiDL project from netlist...") + skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" + skidl_dir.mkdir(parents=True, exist_ok=True) + + try: + subprocess.run([ + 'netlist_to_skidl', + '-i', str(current_netlist), + '--output', str(skidl_dir) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e + + skidl_source = skidl_dir + logging.info(f"✓ Generated SKiDL project: {skidl_dir}") + + # Use direct SKiDL source if provided + elif args.skidl: + skidl_source = Path(args.skidl) + if not skidl_source.exists(): + raise FileNotFoundError(f"SKiDL file not found: {skidl_source}") + logging.info(f"Using SKiDL file: {skidl_source}") - elif args.netlist: - current_netlist = Path(args.netlist) - if not current_netlist.exists(): - raise FileNotFoundError(f"Netlist not found: {current_netlist}") - logging.info(f"Using existing netlist: {current_netlist}") + elif args.skidl_dir: + skidl_source = Path(args.skidl_dir) + if not skidl_source.exists(): + raise FileNotFoundError(f"SKiDL directory not found: {skidl_source}") + logging.info(f"Using SKiDL project directory: {skidl_source}") - # Add and validate custom KiCad library paths - from skidl import lib_search_paths, KICAD + # Add KiCad library paths if provided if args.kicad_lib_paths: + from skidl import lib_search_paths, KICAD valid_paths = [] invalid_paths = [] @@ -683,46 +679,16 @@ def main(): logging.warning("Some library paths were invalid:") for path, reason in invalid_paths: logging.warning(f" ✗ {path}: {reason}") - logging.warning("\nPlease ensure your library paths:") - logging.warning("1. Are valid directory paths") - logging.warning("2. Contain KiCad 8 symbol libraries (.kicad_sym files)") - logging.warning("3. Have correct permissions") - # Step 2: Generate SKiDL project if requested. - if current_netlist and args.generate_skidl: - logging.info("Step 2: Generating SKiDL project from netlist...") - skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" - skidl_dir.mkdir(parents=True, exist_ok=True) - - try: - subprocess.run([ - 'netlist_to_skidl', - '-i', str(current_netlist), - '--output', str(skidl_dir) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError( - f"SKiDL project generation failed:\n{e.stderr}" - ) from e - - current_skidl = skidl_dir - logging.info(f"✓ Generated SKiDL project: {skidl_dir}") - - elif args.skidl_source: - current_skidl = Path(args.skidl_source) - if not current_skidl.exists(): - raise FileNotFoundError(f"SKiDL source not found: {current_skidl}") - logging.info(f"Using existing SKiDL source: {current_skidl}") - - # Step 3: Run analysis if requested. + # Step 3: Run analysis if requested if args.analyze: - if not current_skidl: + if not skidl_source: raise ValueError("No SKiDL source available for analysis") logging.info("Step 3: Analyzing circuits...") try: results = CircuitAnalyzer.analyze_circuits( - source=current_skidl, + source=skidl_source, output_file=args.analysis_output, api_key=args.api_key, backend=args.backend, @@ -733,19 +699,28 @@ def main(): if results["success"]: logging.info("Analysis Results:") - for hier, analysis in results["subcircuits"].items(): - logging.info(f"\nSubcircuit: {hier}") - if analysis["success"]: - logging.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) - if tokens: - logging.info(f" Tokens used: {tokens}") - else: - logging.error(f"✗ Analysis failed: {analysis['error']}") - - logging.info(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") - if results.get('total_tokens'): - logging.info(f"Total tokens used: {results['total_tokens']}") + # Handle subcircuit results if present + if "subcircuits" in results: + for hier, analysis in results["subcircuits"].items(): + logging.info(f"\nSubcircuit: {hier}") + if analysis["success"]: + logging.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) + if tokens: + logging.info(f" Tokens used: {tokens}") + else: + logging.error(f"✗ Analysis failed: {analysis['error']}") + + logging.info(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + if results.get('total_tokens'): + logging.info(f"Total tokens used: {results['total_tokens']}") + else: + # Single circuit analysis results + logging.info(f"✓ Analysis completed in {results['request_time_seconds']:.2f} seconds") + tokens = results.get('prompt_tokens', 0) + results.get('response_tokens', 0) + if tokens: + logging.info(f"Tokens used: {tokens}") + logging.info(f"Analysis results saved to: {args.analysis_output}") else: raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") @@ -770,6 +745,5 @@ def main(): traceback.print_exc() sys.exit(1) - if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/src/skidl/circuit_analyzer.py b/src/skidl/circuit_analyzer.py index c07299507..0607b2098 100644 --- a/src/skidl/circuit_analyzer.py +++ b/src/skidl/circuit_analyzer.py @@ -12,7 +12,7 @@ # API configuration OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" OLLAMA_API_URL = "http://localhost:11434/api/chat" -DEFAULT_MODEL = "google/gemini-flash-1.5" +DEFAULT_MODEL = "google/gemini-2.0-flash-001" DEFAULT_OLLAMA_MODEL = "llama3.2:latest" DEFAULT_TIMEOUT = 30 DEFAULT_TEMPERATURE = 0.7 From a895d730562404aa2ca8eacde512110d44ae8d62 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Thu, 6 Feb 2025 21:11:02 -0800 Subject: [PATCH 62/73] update prompts to look for capacitance derating --- src/skidl/prompts/sections.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/skidl/prompts/sections.py b/src/skidl/prompts/sections.py index c8211e6b7..b557e35d8 100644 --- a/src/skidl/prompts/sections.py +++ b/src/skidl/prompts/sections.py @@ -110,6 +110,16 @@ - Debug interface requirements """.strip() +CAP_DC_BIAS_DERATING: str = """ +7. Capacitor DC Bias Derating Analysis: +- Identification of capacitors susceptible to DC bias derating +- Calculation of effective capacitance at operating voltage +- Impact on circuit performance (e.g., filter cutoff frequency, timing) +- Recommendation of alternative capacitor types or values +- Consideration of temperature effects on DC bias derating +- Verification of capacitance derating with manufacturer data +""".strip() + # Dictionary mapping section names to their prompts ANALYSIS_SECTIONS: Dict[str, str] = { "system_overview": SYSTEM_OVERVIEW, @@ -118,5 +128,6 @@ "signal_integrity": SIGNAL_INTEGRITY, "thermal_analysis": THERMAL_ANALYSIS, "noise_analysis": NOISE_ANALYSIS, - "testing_verification": TESTING_VERIFICATION + "testing_verification": TESTING_VERIFICATION, + "cap_dc_bias_derating": CAP_DC_BIAS_DERATING } From a79b80d5a901df95d82ebbd44872299969dc695f Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Thu, 6 Feb 2025 21:44:15 -0800 Subject: [PATCH 63/73] update script to remove duplicate parsing and use skidl project default_circuit instead of re-assembling the skidl circuit one by one --- kicad_skidl_llm.py | 395 ++++++++++++++++----------------------------- 1 file changed, 135 insertions(+), 260 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index bcd47f063..7cb1e0905 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -80,11 +80,23 @@ import logging from skidl import * -# Set up basic logging configuration. -logging.basicConfig( - level=logging.INFO, - format='[%(levelname)s] %(message)s' -) +import time +from datetime import datetime + +# Configure logging +logger = logging.getLogger('kicad_skidl_llm') +logger.setLevel(logging.INFO) +formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') +handler = logging.StreamHandler() +handler.setFormatter(formatter) +logger.addHandler(handler) + +# Disable propagation of SKiDL loggers to prevent duplicate messages +skidl_loggers = ['skidl.logger', 'skidl.active_logger', 'skidl.erc_logger'] +for name in skidl_loggers: + l = logging.getLogger(name) + l.propagate = False # Custom exception to signal issues during circuit discovery and loading. class CircuitDiscoveryError(Exception): @@ -109,130 +121,6 @@ class CircuitAnalyzer: potentially unsafe code, then creates isolated environments for circuit execution and analysis. """ - - @staticmethod - def find_subcircuits(directory: Path) -> List[Dict[str, str]]: - """ - Recursively find all functions decorated with @subcircuit in Python files. - - Uses AST parsing to safely discover circuits without executing code. - Skips files that can't be parsed and logs warnings. - - Args: - directory: Root directory to search for Python files - - Returns: - List of dicts containing: - - 'file': Path to Python file - - 'function': Name of decorated function - - 'lineno': Line number where the function is defined - - Raises: - CircuitDiscoveryError: If no valid Python files found - """ - subcircuits = [] - - class SubcircuitFinder(ast.NodeVisitor): - """AST visitor that identifies @subcircuit decorated functions.""" - def visit_FunctionDef(self, node): - for decorator in node.decorator_list: - if isinstance(decorator, ast.Name) and decorator.id == 'subcircuit': - subcircuits.append({ - 'file': str(current_file), - 'function': node.name, - 'lineno': node.lineno - }) - - python_files = list(directory.rglob('*.py')) - if not python_files: - raise CircuitDiscoveryError(f"No Python files found in {directory}") - - for current_file in python_files: - try: - with open(current_file, 'r', encoding='utf-8') as f: - tree = ast.parse(f.read()) - SubcircuitFinder().visit(tree) - except Exception as e: - logging.warning(f"Could not parse {current_file}: {e}") - - return subcircuits - - @staticmethod - def create_analysis_module( - subcircuits: List[Dict[str, str]], - output_dir: Path - ) -> Tuple[Path, str]: - """ - Create a temporary Python module that imports and executes discovered subcircuits. - The generated module creates dummy SKiDL nets for any parameters required by the - subcircuit functions, allowing them to be called without errors. - - This version groups the discovered functions by file and generates explicit - import statements (rather than wildcard imports) so that functions whose names - start with an underscore (e.g. _3v3_regulator) are imported correctly. - - Args: - subcircuits: List of subcircuit information from find_subcircuits() - output_dir: Directory to write temporary module - - Returns: - Tuple of: - - Path to created module - - Generated code string (for debugging) - - Raises: - CircuitDiscoveryError: If module creation fails - """ - analysis_file = output_dir / 'circuit_analysis.py' - try: - # Generate import statements and helper function. - code_lines = [ - "from skidl import *\n", - "from skidl.tools import *\n", - "import inspect\n\n", - "# Helper function that creates dummy nets for required parameters.\n", - "def call_subcircuit(func):\n", - " # If the function is wrapped, get the original.\n", - " wrapped = getattr(func, '__wrapped__', func)\n", - " sig = inspect.signature(wrapped)\n", - " # Create a dummy net for each parameter\n", - " dummy_args = [Net(param.name) for param in sig.parameters.values()]\n", - " return func(*dummy_args)\n\n" - ] - - # Group discovered functions by file. - imports_by_file = {} - for s in subcircuits: - file = s['file'] - func_name = s['function'] - if file not in imports_by_file: - imports_by_file[file] = set() - imports_by_file[file].add(func_name) - - # Generate explicit import lines for each file. - for file, functions in imports_by_file.items(): - module_name = Path(file).stem - funcs_str = ", ".join(sorted(functions)) - code_lines.append(f"from {module_name} import {funcs_str}\n") - - # Create main function to execute each subcircuit via call_subcircuit. - code_lines.append("\n\ndef main():\n") - for s in subcircuits: - func_name = s['function'] - code_lines.append(f" call_subcircuit({func_name})\n") - - code = ''.join(code_lines) - - with open(analysis_file, 'w', encoding='utf-8') as f: - f.write(code) - - return analysis_file, code - - except Exception as e: - raise CircuitDiscoveryError( - f"Failed to create analysis module: {str(e)}" - ) from e - @staticmethod def analyze_circuits( @@ -245,124 +133,109 @@ def analyze_circuits( dump_temp: bool = False ) -> dict: """ - Analyze circuits from either a SKiDL source file/project or KiCad conversion. - - Args: - source: Path to SKiDL file/project or KiCad conversion directory - output_file: Where to save analysis results - api_key: API key for cloud LLM service (required for openrouter) - backend: 'openrouter' or 'ollama' - model: Model name for selected backend - prompt: Custom analysis prompt - dump_temp: If True, do not delete temporary analysis files - - Returns: - Analysis results dictionary + Analyze circuits by executing original SKiDL code and analyzing default_circuit. """ + start_time = time.time() + + # Generate a meaningful output filename if none provided + if not output_file: + output_file = f"{source.stem}_analysis.txt" + if source.is_file(): - # Direct SKiDL file case - import and analyze the circuit - logging.info(f"Analyzing SKiDL file: {source}") + logger.info(f"Starting analysis of SKiDL file: {source}") sys.path.insert(0, str(source.parent)) try: - # Import the SKiDL module which will populate default_circuit + # Import phase + t0 = time.time() + logger.info("Importing SKiDL module...") module = importlib.import_module(source.stem) - importlib.reload(module) # Reload in case it was previously imported + importlib.reload(module) + logger.info(f"Import completed in {time.time() - t0:.2f} seconds") - # If there's a main() function, call it to instantiate the circuit + # Circuit instantiation phase if hasattr(module, 'main'): + t0 = time.time() + logger.info("Executing circuit main()...") module.main() - - # Get the populated default_circuit and analyze it - return default_circuit.analyze_with_llm( + logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + + # LLM Analysis phase + t0 = time.time() + logger.info("Starting LLM analysis...") + results = default_circuit.analyze_with_llm( api_key=api_key, output_file=output_file, backend=backend, model=model, - prompt=prompt, + custom_prompt=prompt, analyze_subcircuits=True ) + logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") + + total_time = time.time() - start_time + logger.info(f"Total processing time: {total_time:.2f} seconds") + return results except Exception as e: - raise CircuitDiscoveryError( - f"Failed to analyze SKiDL file {source}: {str(e)}" - ) from e + logger.error(f"Analysis failed: {str(e)}") + raise finally: sys.path.pop(0) else: - # Directory case - handle KiCad conversion output - logging.info(f"Analyzing KiCad conversion directory: {source}") + # Directory case - import main.py from converted project + logger.info(f"Starting analysis of KiCad conversion directory: {source}") + main_file = source / 'main.py' + if not main_file.exists(): + raise CircuitDiscoveryError(f"No main.py found in {source}") + + sys.path.insert(0, str(source)) try: - # Find subcircuits in the converted SKiDL project - subcircuits = CircuitAnalyzer.find_subcircuits(source) - if not subcircuits: - raise CircuitDiscoveryError( - f"No @subcircuit functions found in {source}" - ) - - logging.info(f"Found {len(subcircuits)} circuits to analyze:") - for circuit in subcircuits: - logging.info( - f" - {circuit['function']} ({circuit['file']}:{circuit['lineno']})" - ) - - # For KiCad conversions, we still need to create an analysis module - # since we're working with generated SKiDL code - analysis_module, generated_code = CircuitAnalyzer.create_analysis_module( - subcircuits, source - ) - - # Execute the generated module - sys.path.insert(0, str(source)) + # Import phase + t0 = time.time() + logger.info("Importing project main module...") + module = importlib.import_module('main') + importlib.reload(module) + logger.info(f"Import completed in {time.time() - t0:.2f} seconds") + + # Circuit instantiation phase + t0 = time.time() + logger.info("Executing circuit main()...") try: - module = importlib.import_module('circuit_analysis') - importlib.reload(module) - try: - module.main() - except FileNotFoundError as e: - # Check if this is a KiCad library error - if "Can't open file:" in str(e): - missing_lib = str(e).split(':')[-1].strip() - msg = ( - f"KiCad symbol library not found: {missing_lib}\n\n" - "Use --kicad-lib-paths to specify library directories:\n" - " --kicad-lib-paths /path/to/symbol/libraries/\n" - ) - raise CircuitDiscoveryError(msg) from e - raise - except Exception as e: - raise CircuitDiscoveryError( - f"Circuit execution failed: {str(e)}" - ) from e - - # Analyze the populated default_circuit - return default_circuit.analyze_with_llm( - api_key=api_key, - output_file=output_file, - backend=backend, - model=model, - custom_prompt=prompt, - analyze_subcircuits=True - ) - - finally: - sys.path.pop(0) - # Clean up analysis module if it exists and not dumping temp files - if not dump_temp: - try: - analysis_module.unlink() - except Exception as e: - logging.warning(f"Failed to remove temporary module: {e}") - else: - logging.info(f"Temporary analysis module saved at: {analysis_module}") - - except CircuitDiscoveryError: - raise - except Exception as e: - raise CircuitDiscoveryError(f"Circuit analysis failed: {str(e)}") from e - + module.main() + except FileNotFoundError as e: + if "Can't open file:" in str(e): + missing_lib = str(e).split(':')[-1].strip() + msg = ( + f"KiCad symbol library not found: {missing_lib}\n" + "Use --kicad-lib-paths to specify library directories" + ) + raise CircuitDiscoveryError(msg) from e + raise + logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + + # LLM Analysis phase + t0 = time.time() + logger.info("Starting LLM analysis...") + results = default_circuit.analyze_with_llm( + api_key=api_key, + output_file=output_file, + backend=backend, + model=model, + custom_prompt=prompt, + analyze_subcircuits=True + ) + logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") + + total_time = time.time() - start_time + logger.info(f"Total processing time: {total_time:.2f} seconds") + return results + + finally: + sys.path.pop(0) + def validate_kicad_cli(path: str) -> str: """ Validate that KiCad CLI exists and is executable. @@ -569,13 +442,16 @@ def main(): 2. Analysis of SKiDL project directories 3. KiCad schematic conversion and analysis """ + start_time = time.time() try: + logger.info("Starting KiCad-SKiDL-LLM pipeline") args = parse_args() # Determine output directory + t0 = time.time() output_dir = Path(args.output_dir) output_dir.mkdir(parents=True, exist_ok=True) - + logger.info(f"Created output directory: {output_dir}") # Track the SKiDL source to analyze skidl_source = None @@ -585,7 +461,7 @@ def main(): # Step 1: Generate netlist if requested if args.schematic and args.generate_netlist: - logging.info("Step 1: Generating netlist from schematic...") + logger.info("Step 1: Generating netlist from schematic...") schematic_path = Path(args.schematic) if not schematic_path.exists(): raise FileNotFoundError(f"Schematic not found: {schematic_path}") @@ -604,17 +480,17 @@ def main(): raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e current_netlist = netlist_path - logging.info(f"✓ Generated netlist: {netlist_path}") + logger.info(f"✓ Generated netlist: {netlist_path}") elif args.netlist: current_netlist = Path(args.netlist) if not current_netlist.exists(): raise FileNotFoundError(f"Netlist not found: {current_netlist}") - logging.info(f"Using existing netlist: {current_netlist}") + logger.info(f"Using existing netlist: {current_netlist}") # Step 2: Generate SKiDL if requested if args.generate_skidl: - logging.info("Step 2: Generating SKiDL project from netlist...") + logger.info("Step 2: Generating SKiDL project from netlist...") skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" skidl_dir.mkdir(parents=True, exist_ok=True) @@ -628,20 +504,20 @@ def main(): raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e skidl_source = skidl_dir - logging.info(f"✓ Generated SKiDL project: {skidl_dir}") + logger.info(f"✓ Generated SKiDL project: {skidl_dir}") # Use direct SKiDL source if provided elif args.skidl: skidl_source = Path(args.skidl) if not skidl_source.exists(): raise FileNotFoundError(f"SKiDL file not found: {skidl_source}") - logging.info(f"Using SKiDL file: {skidl_source}") + logger.info(f"Using SKiDL file: {skidl_source}") elif args.skidl_dir: skidl_source = Path(args.skidl_dir) if not skidl_source.exists(): raise FileNotFoundError(f"SKiDL directory not found: {skidl_source}") - logging.info(f"Using SKiDL project directory: {skidl_source}") + logger.info(f"Using SKiDL project directory: {skidl_source}") # Add KiCad library paths if provided if args.kicad_lib_paths: @@ -666,26 +542,26 @@ def main(): # Log results if valid_paths: - logging.info("Added KiCad 8 library paths:") + logger.info("Added KiCad 8 library paths:") for path in valid_paths: - logging.info(f" ✓ {path}") + logger.info(f" ✓ {path}") sym_files = list(path.glob("*.kicad_sym")) for sym in sym_files[:3]: # Show up to 3 symbol libraries - logging.info(f" - {sym.name}") + logger.info(f" - {sym.name}") if len(sym_files) > 3: - logging.info(f" - ... and {len(sym_files)-3} more") + logger.info(f" - ... and {len(sym_files)-3} more") if invalid_paths: - logging.warning("Some library paths were invalid:") + logger.warning("Some library paths were invalid:") for path, reason in invalid_paths: - logging.warning(f" ✗ {path}: {reason}") + logger.warning(f" ✗ {path}: {reason}") # Step 3: Run analysis if requested if args.analyze: if not skidl_source: raise ValueError("No SKiDL source available for analysis") - logging.info("Step 3: Analyzing circuits...") + logger.info("Step 3: Analyzing circuits...") try: results = CircuitAnalyzer.analyze_circuits( source=skidl_source, @@ -698,51 +574,50 @@ def main(): ) if results["success"]: - logging.info("Analysis Results:") + logger.info("Analysis Results:") # Handle subcircuit results if present if "subcircuits" in results: for hier, analysis in results["subcircuits"].items(): - logging.info(f"\nSubcircuit: {hier}") + logger.info(f"\nSubcircuit: {hier}") if analysis["success"]: - logging.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") + logger.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) if tokens: - logging.info(f" Tokens used: {tokens}") + logger.info(f" Tokens used: {tokens}") else: - logging.error(f"✗ Analysis failed: {analysis['error']}") + logger.error(f"✗ Analysis failed: {analysis['error']}") - logging.info(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") + logger.info(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") if results.get('total_tokens'): - logging.info(f"Total tokens used: {results['total_tokens']}") + logger.info(f"Total tokens used: {results['total_tokens']}") else: # Single circuit analysis results - logging.info(f"✓ Analysis completed in {results['request_time_seconds']:.2f} seconds") + logger.info(f"✓ Analysis completed in {results['request_time_seconds']:.2f} seconds") tokens = results.get('prompt_tokens', 0) + results.get('response_tokens', 0) if tokens: - logging.info(f"Tokens used: {tokens}") + logger.info(f"Tokens used: {tokens}") - logging.info(f"Analysis results saved to: {args.analysis_output}") + logger.info(f"Analysis results saved to: {args.analysis_output}") else: raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") except Exception as e: - logging.error("✗ Circuit analysis failed!") - logging.error(f"Error: {str(e)}") + logger.error("✗ Circuit analysis failed!") + logger.error(f"Error: {str(e)}") if args.backend == 'openrouter': - logging.error("Troubleshooting tips:") - logging.error("1. Check your API key") - logging.error("2. Verify you have sufficient API credits") - logging.error("3. Check for rate limiting") + logger.error("Troubleshooting tips:") + logger.error("1. Check your API key") + logger.error("2. Verify you have sufficient API credits") + logger.error("3. Check for rate limiting") else: - logging.error("Troubleshooting tips:") - logging.error("1. Verify Ollama is running locally") - logging.error("2. Check if the requested model is installed") + logger.error("Troubleshooting tips:") + logger.error("1. Verify Ollama is running locally") + logger.error("2. Check if the requested model is installed") raise except Exception as e: - logging.error("Error: %s", str(e)) - logging.error("Stack trace:") - traceback.print_exc() + logger.error(f"Pipeline failed: {str(e)}") + logger.debug("Stack trace:", exc_info=True) sys.exit(1) if __name__ == "__main__": From 45952125d077ba575190a72195d3fc881b52204f Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Thu, 6 Feb 2025 22:07:44 -0800 Subject: [PATCH 64/73] add function to skip subcircuits --- kicad_skidl_llm.py | 843 +++++++++++++++++++++++---------------------- 1 file changed, 438 insertions(+), 405 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index 7cb1e0905..c5aa4fe6e 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -3,85 +3,53 @@ KiCad-SKiDL-LLM Pipeline ------------------------ -This script provides a flexible pipeline for working with KiCad schematics, -netlists, SKiDL projects, and LLM-based circuit analysis. It supports multiple -entry points and operation modes: +A tool for analyzing KiCad circuit designs using SKiDL and LLMs. +Supports direct SKiDL analysis, KiCad schematic conversion, and netlist processing. -Entry Points: - - KiCad schematic (.kicad_sch) - - Netlist (.net) - - SKiDL project (single file or directory) +Features: + - KiCad schematic (.kicad_sch) to netlist conversion + - Netlist to SKiDL project generation + - Circuit analysis using OpenRouter or Ollama LLM backends + - Support for subcircuit analysis with selective circuit skipping + - Detailed logging and timing information -Operations: - - Generate netlist from KiCad schematic - - Convert netlist to SKiDL project - - Analyze circuits using LLM (local or cloud-based) - -Key Features: - - Safe discovery and analysis of SKiDL circuits using AST parsing - - Support for both single-file and multi-file SKiDL projects - - Flexible LLM backend selection: - * OpenRouter (cloud-based): Note that model names must be set according - to the OpenRouter naming standard. - * Ollama (local): If using this backend, ensure Ollama is installed locally. - - Comprehensive error handling and reporting - -Additional Notes for Novice Users: - - Make sure you have KiCad 7.0+ installed. You must also point the script to the - correct location of your KiCad CLI executable (using --kicad-cli) if it is not in the default path. - - For LLM analysis using OpenRouter, you must provide a valid API key with --api-key. - - Ensure that any LLM model name provided (via --model) adheres to the naming conventions - required by the selected backend. - -Usage Examples: - # Generate netlist and SKiDL project from schematic - python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist --generate-skidl +Example Usage: + # Analyze KiCad schematic + python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist --generate-skidl --analyze --api-key YOUR_KEY - # Analyze existing SKiDL project using OpenRouter - python kicad_skidl_llm.py --skidl-source myproject/ --analyze --api-key YOUR_KEY + # Analyze existing SKiDL project using Ollama + python kicad_skidl_llm.py --skidl-dir project/ --analyze --backend ollama - # Generate SKiDL from netlist and analyze using Ollama - python kicad_skidl_llm.py --netlist circuit.net --generate-skidl --analyze --backend ollama - - # Dump temporary analysis file for inspection - python kicad_skidl_llm.py --skidl-source myproject/ --analyze --api-key YOUR_KEY --dump-temp - -Known Limitations: - 1. Memory usage may be high for projects with many subcircuits. - 2. Python path handling may need adjustment for complex project structures. - 3. Circular dependencies in SKiDL projects may cause issues. - 4. File encoding issues possible with non-UTF8 files. - 5. Large projects may hit API rate limits with cloud LLMs. - -Dependencies: - - Python 3.7+ - - KiCad 7.0+ (for schematic operations) - - SKiDL - - AST (standard library) - - Either OpenRouter API key (for cloud-based LLM analysis) or a local Ollama installation - -Author: [Your Name] -Date: February 2024 -License: MIT + # Skip specific circuits during analysis + python kicad_skidl_llm.py --skidl circuit.py --analyze --skip-circuits "voltage_regulator1,adc_interface2" """ import os import sys -import ast -import inspect import platform import subprocess import importlib import textwrap import traceback from pathlib import Path -from typing import List, Dict, Optional, Tuple +from typing import List, Dict, Optional, Union, Tuple, Set import argparse import logging +from datetime import datetime +import time +from enum import Enum + from skidl import * -import time -from datetime import datetime +# Global configuration +DEFAULT_TIMEOUT = 300 +DEFAULT_MODEL = "google/gemini-2.0-flash-001" +DEFAULT_OUTPUT_DIR = Path.cwd() / "output" + +class Backend(Enum): + """Supported LLM backends.""" + OPENROUTER = "openrouter" + OLLAMA = "ollama" # Configure logging logger = logging.getLogger('kicad_skidl_llm') @@ -98,153 +66,262 @@ l = logging.getLogger(name) l.propagate = False -# Custom exception to signal issues during circuit discovery and loading. class CircuitDiscoveryError(Exception): """Raised when there are issues discovering or loading circuits.""" pass -class CircuitAnalyzer: - """ - Handles discovery and analysis of SKiDL circuits. - - This class provides methods for: - - Finding @subcircuit decorated functions in Python files - - Safely loading and executing discovered circuits - - Running LLM analysis on circuits - Note for LLM Analysis: - The default LLM analysis uses OpenRouter. This means that any LLM model name - provided must follow OpenRouter's naming standards. Alternatively, you can choose - the 'ollama' backend for local analysis if you have Ollama installed. - - The analyzer uses AST parsing to discover circuits without executing - potentially unsafe code, then creates isolated environments for - circuit execution and analysis. + +def analyze_circuits( + source: Path, + output_file: Path, + api_key: Optional[str] = None, + backend: Backend = Backend.OPENROUTER, + model: Optional[str] = None, + prompt: Optional[str] = None, + skip_circuits: Optional[Set[str]] = None, + dump_temp: bool = False +) -> Dict[str, Union[bool, float, int, Dict]]: + """Analyze SKiDL circuits using the specified LLM backend. + + Args: + source: Path to SKiDL file or directory + output_file: Output file for analysis results + api_key: API key for LLM service + backend: LLM backend to use + model: Model name for selected backend + prompt: Custom analysis prompt + skip_circuits: Set of circuit hierarchies to skip + dump_temp: Flag to dump temporary files """ + start_time = time.time() + skip_circuits = skip_circuits or set() - @staticmethod - def analyze_circuits( - source: Path, - output_file: str, - api_key: Optional[str] = None, - backend: str = 'openrouter', - model: Optional[str] = None, - prompt: Optional[str] = None, - dump_temp: bool = False - ) -> dict: - """ - Analyze circuits by executing original SKiDL code and analyzing default_circuit. - """ - start_time = time.time() + if skip_circuits: + logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") + + if source.is_file(): + logger.info(f"Starting analysis of SKiDL file: {source}") - # Generate a meaningful output filename if none provided - if not output_file: - output_file = f"{source.stem}_analysis.txt" - - if source.is_file(): - logger.info(f"Starting analysis of SKiDL file: {source}") + sys.path.insert(0, str(source.parent)) + try: + # Import phase + t0 = time.time() + logger.info("Importing SKiDL module...") + module = importlib.import_module(source.stem) + importlib.reload(module) + logger.info(f"Import completed in {time.time() - t0:.2f} seconds") - sys.path.insert(0, str(source.parent)) - try: - # Import phase + # Circuit instantiation phase + if hasattr(module, 'main'): t0 = time.time() - logger.info("Importing SKiDL module...") - module = importlib.import_module(source.stem) - importlib.reload(module) - logger.info(f"Import completed in {time.time() - t0:.2f} seconds") + logger.info("Executing circuit main()...") + module.main() + logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + + # LLM Analysis phase + t0 = time.time() + logger.info("Starting LLM analysis...") + + # Get hierarchies from parts and filter out skipped ones + hierarchies = set() + for part in default_circuit.parts: + if part.hierarchy != default_circuit.hierarchy: # Skip top level + if part.hierarchy not in skip_circuits: + hierarchies.add(part.hierarchy) + + if not hierarchies: + logger.warning("No valid circuits found for analysis after applying skip filters") + return { + "success": True, + "subcircuits": {}, + "total_time_seconds": time.time() - start_time, + "total_tokens": 0 + } + + # Generate consolidated results + results = { + "success": True, + "subcircuits": {}, + "total_time_seconds": 0, + "total_tokens": 0 + } + + # Analyze each non-skipped subcircuit + consolidated_text = ["=== Subcircuits Analysis ===\n"] + for hier in sorted(hierarchies): + logger.info(f"\nAnalyzing subcircuit: {hier}") - # Circuit instantiation phase - if hasattr(module, 'main'): - t0 = time.time() - logger.info("Executing circuit main()...") - module.main() - logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + # Get description focused on this subcircuit + circuit_desc = default_circuit.get_circuit_info(hierarchy=hier, depth=1) - # LLM Analysis phase - t0 = time.time() - logger.info("Starting LLM analysis...") - results = default_circuit.analyze_with_llm( + # Analyze just this subcircuit + sub_results = default_circuit.analyze_with_llm( api_key=api_key, - output_file=output_file, - backend=backend, - model=model, + output_file=None, # Don't write individual files + backend=backend.value, + model=model or DEFAULT_MODEL, custom_prompt=prompt, - analyze_subcircuits=True + analyze_subcircuits=False # Only analyze this specific subcircuit ) - logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") - total_time = time.time() - start_time - logger.info(f"Total processing time: {total_time:.2f} seconds") - return results + results["subcircuits"][hier] = sub_results + results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) + results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("completion_tokens", 0) - except Exception as e: - logger.error(f"Analysis failed: {str(e)}") - raise - finally: - sys.path.pop(0) + # Build consolidated output text + consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") + if sub_results.get("success", False): + analysis_text = sub_results.get("analysis", "No analysis available") + consolidated_text.append(analysis_text) - else: - # Directory case - import main.py from converted project - logger.info(f"Starting analysis of KiCad conversion directory: {source}") + token_info = ( + f"\nTokens used: {sub_results.get('total_tokens', 0)} " + f"(Prompt: {sub_results.get('prompt_tokens', 0)}, " + f"Completion: {sub_results.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + else: + consolidated_text.append( + f"Analysis failed: {sub_results.get('error', 'Unknown error')}" + ) + consolidated_text.append("\n") - main_file = source / 'main.py' - if not main_file.exists(): - raise CircuitDiscoveryError(f"No main.py found in {source}") - - sys.path.insert(0, str(source)) + # Save consolidated results + if output_file: + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") + total_time = time.time() - start_time + logger.info(f"Total processing time: {total_time:.2f} seconds") + + return results + + except Exception as e: + logger.error(f"Analysis failed: {str(e)}") + raise + finally: + sys.path.pop(0) + + else: + # Directory case - import main.py from converted project + logger.info(f"Starting analysis of KiCad conversion directory: {source}") + + main_file = source / 'main.py' + if not main_file.exists(): + raise CircuitDiscoveryError(f"No main.py found in {source}") + + sys.path.insert(0, str(source)) + try: + # Import phase + t0 = time.time() + logger.info("Importing project main module...") + module = importlib.import_module('main') + importlib.reload(module) + logger.info(f"Import completed in {time.time() - t0:.2f} seconds") + + # Circuit instantiation phase + t0 = time.time() + logger.info("Executing circuit main()...") try: - # Import phase - t0 = time.time() - logger.info("Importing project main module...") - module = importlib.import_module('main') - importlib.reload(module) - logger.info(f"Import completed in {time.time() - t0:.2f} seconds") + module.main() + except FileNotFoundError as e: + if "Can't open file:" in str(e): + missing_lib = str(e).split(':')[-1].strip() + msg = ( + f"KiCad symbol library not found: {missing_lib}\n" + "Use --kicad-lib-paths to specify library directories" + ) + raise CircuitDiscoveryError(msg) from e + raise + logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + + # LLM Analysis phase + t0 = time.time() + logger.info("Starting LLM analysis...") + + # Get hierarchies and filter out skipped ones + hierarchies = set() + for part in default_circuit.parts: + if part.hierarchy != default_circuit.hierarchy: # Skip top level + if part.hierarchy not in skip_circuits: + hierarchies.add(part.hierarchy) + + if not hierarchies: + logger.warning("No valid circuits found for analysis after applying skip filters") + return { + "success": True, + "subcircuits": {}, + "total_time_seconds": time.time() - start_time, + "total_tokens": 0 + } + + # Generate consolidated results + results = { + "success": True, + "subcircuits": {}, + "total_time_seconds": 0, + "total_tokens": 0 + } + + # Analyze each non-skipped subcircuit + consolidated_text = ["=== Subcircuits Analysis ===\n"] + for hier in sorted(hierarchies): + logger.info(f"\nAnalyzing subcircuit: {hier}") - # Circuit instantiation phase - t0 = time.time() - logger.info("Executing circuit main()...") - try: - module.main() - except FileNotFoundError as e: - if "Can't open file:" in str(e): - missing_lib = str(e).split(':')[-1].strip() - msg = ( - f"KiCad symbol library not found: {missing_lib}\n" - "Use --kicad-lib-paths to specify library directories" - ) - raise CircuitDiscoveryError(msg) from e - raise - logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + # Get description focused on this subcircuit + circuit_desc = default_circuit.get_circuit_info(hierarchy=hier, depth=1) - # LLM Analysis phase - t0 = time.time() - logger.info("Starting LLM analysis...") - results = default_circuit.analyze_with_llm( + # Analyze just this subcircuit + sub_results = default_circuit.analyze_with_llm( api_key=api_key, - output_file=output_file, - backend=backend, - model=model, + output_file=None, # Don't write individual files + backend=backend.value, + model=model or DEFAULT_MODEL, custom_prompt=prompt, - analyze_subcircuits=True + analyze_subcircuits=False # Only analyze this specific subcircuit ) - logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") - total_time = time.time() - start_time - logger.info(f"Total processing time: {total_time:.2f} seconds") - return results + results["subcircuits"][hier] = sub_results + results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) + results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("completion_tokens", 0) - finally: - sys.path.pop(0) - + # Build consolidated output text + consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") + if sub_results.get("success", False): + analysis_text = sub_results.get("analysis", "No analysis available") + consolidated_text.append(analysis_text) + + token_info = ( + f"\nTokens used: {sub_results.get('total_tokens', 0)} " + f"(Prompt: {sub_results.get('prompt_tokens', 0)}, " + f"Completion: {sub_results.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + else: + consolidated_text.append( + f"Analysis failed: {sub_results.get('error', 'Unknown error')}" + ) + consolidated_text.append("\n") + + # Save consolidated results + if output_file: + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") + total_time = time.time() - start_time + logger.info(f"Total processing time: {total_time:.2f} seconds") + + return results + + finally: + sys.path.pop(0) + def validate_kicad_cli(path: str) -> str: """ - Validate that KiCad CLI exists and is executable. - Provides platform-specific guidance if validation fails. - - Common paths by platform: - - macOS: /Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli - - Windows: C:\\Program Files\\KiCad\\7.0\\bin\\kicad-cli.exe - - Linux: /usr/bin/kicad-cli + Validate KiCad CLI executable and provide platform-specific guidance. Args: path: Path to kicad-cli executable @@ -253,11 +330,9 @@ def validate_kicad_cli(path: str) -> str: Validated path Raises: - FileNotFoundError: If executable not found, with platform-specific guidance - PermissionError: If executable lacks permissions, with remediation steps + FileNotFoundError: If executable not found + PermissionError: If executable lacks permissions """ - import platform - system = platform.system().lower() cli_path = Path(path) @@ -299,126 +374,57 @@ def validate_kicad_cli(path: str) -> str: return str(cli_path) +def get_default_kicad_cli() -> str: + """Get the default KiCad CLI path based on the current platform.""" + system = platform.system().lower() + if system == 'darwin': # macOS + return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + elif system == 'windows': + return r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe" + else: # Linux and others + return "/usr/bin/kicad-cli" def parse_args() -> argparse.Namespace: - """ - Parse and validate command line arguments. - - Returns: - Parsed argument namespace - - Raises: - argparse.ArgumentError: For invalid argument combinations - """ + """Parse and validate command line arguments.""" parser = argparse.ArgumentParser( description='KiCad to SKiDL conversion and circuit analysis pipeline', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent(""" Examples: - # Analyze an existing SKiDL file - %(prog)s --skidl my_circuit.py --analyze --api-key YOUR_KEY - - # Generate netlist and SKiDL project from KiCad schematic, then analyze + # Generate netlist and SKiDL project from schematic %(prog)s --schematic design.kicad_sch --generate-netlist --generate-skidl --analyze --api-key YOUR_KEY - # Generate SKiDL from netlist and analyze using Ollama - %(prog)s --netlist circuit.net --generate-skidl --analyze --backend ollama - - # Analyze KiCad-converted SKiDL project - %(prog)s --skidl-dir myproject/ --analyze --api-key YOUR_KEY + # Analyze existing SKiDL project with circuit skipping + %(prog)s --skidl-dir project/ --analyze --skip-circuits "circuit1,circuit2" --api-key YOUR_KEY """) ) # Input source group (mutually exclusive) source_group = parser.add_mutually_exclusive_group(required=True) - source_group.add_argument( - '--schematic', '-s', - help='Path to KiCad schematic (.kicad_sch) file' - ) - source_group.add_argument( - '--netlist', '-n', - help='Path to netlist (.net) file' - ) - source_group.add_argument( - '--skidl', - help='Path to SKiDL Python file to analyze' - ) - source_group.add_argument( - '--skidl-dir', - help='Path to SKiDL project directory (e.g., from KiCad conversion)' - ) + source_group.add_argument('--schematic', '-s', help='Path to KiCad schematic (.kicad_sch) file') + source_group.add_argument('--netlist', '-n', help='Path to netlist (.net) file') + source_group.add_argument('--skidl', help='Path to SKiDL Python file to analyze') + source_group.add_argument('--skidl-dir', help='Path to SKiDL project directory') # Operation mode flags - parser.add_argument( - '--generate-netlist', - action='store_true', - help='Generate netlist from schematic' - ) - parser.add_argument( - '--generate-skidl', - action='store_true', - help='Generate SKiDL project from netlist' - ) - parser.add_argument( - '--analyze', - action='store_true', - help='Run LLM analysis on circuits' - ) + parser.add_argument('--generate-netlist', action='store_true', help='Generate netlist from schematic') + parser.add_argument('--generate-skidl', action='store_true', help='Generate SKiDL project from netlist') + parser.add_argument('--analyze', action='store_true', help='Run LLM analysis on circuits') # Optional configuration - def get_default_kicad_cli() -> str: - """Get the default KiCad CLI path based on the current platform.""" - system = platform.system().lower() - if system == 'darwin': # macOS - return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" - elif system == 'windows': - return "C:\\Program Files\\KiCad\\7.0\\bin\\kicad-cli.exe" - else: # Linux and others - return "/usr/bin/kicad-cli" - - parser.add_argument( - '--kicad-cli', - default=get_default_kicad_cli(), - help='Path to kicad-cli executable (defaults to standard installation path for your OS)' - ) - parser.add_argument( - '--output-dir', '-o', - default='.', - help='Output directory for generated files' - ) - parser.add_argument( - '--api-key', - help='OpenRouter API key for cloud LLM analysis (required for openrouter backend)' - ) - parser.add_argument( - '--backend', - choices=['openrouter', 'ollama'], - default='openrouter', - help='LLM backend to use (default is openrouter)' - ) - parser.add_argument( - '--model', - help='LLM model name for selected backend (ensure the model name conforms to the backend\'s naming standard)' - ) - parser.add_argument( - '--analysis-output', - default='circuit_analysis.txt', - help='Output file for analysis results' - ) - parser.add_argument( - '--analysis-prompt', - help='Custom prompt for circuit analysis' - ) - parser.add_argument( - '--dump-temp', - action='store_true', - help='Keep and output the temporary analysis file for inspection' - ) - parser.add_argument( - '--kicad-lib-paths', - nargs='*', - help='List of custom KiCad library paths (e.g., /path/to/lib1 /path/to/lib2)' - ) + parser.add_argument('--kicad-cli', default=get_default_kicad_cli(), + help='Path to kicad-cli executable') + parser.add_argument('--output-dir', '-o', default='.', help='Output directory for generated files') + parser.add_argument('--api-key', help='OpenRouter API key for cloud LLM analysis') + parser.add_argument('--backend', choices=['openrouter', 'ollama'], default='openrouter', + help='LLM backend to use') + parser.add_argument('--model', help='LLM model name for selected backend') + parser.add_argument('--analysis-output', default='circuit_analysis.txt', + help='Output file for analysis results') + parser.add_argument('--analysis-prompt', help='Custom prompt for circuit analysis') + parser.add_argument('--skip-circuits', help='Comma-separated list of circuits to skip during analysis') + parser.add_argument('--kicad-lib-paths', nargs='*', + help='List of custom KiCad library paths') args = parser.parse_args() @@ -432,156 +438,181 @@ def get_default_kicad_cli() -> str: return args +def setup_environment(args) -> Path: + """Setup output directory and return its path.""" + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + return output_dir + +def handle_kicad_libraries(lib_paths: Optional[List[str]]) -> None: + """Add and validate KiCad library paths.""" + if not lib_paths: + return + + from skidl import lib_search_paths, KICAD + valid_paths = [] + invalid_paths = [] + + for lib_path in lib_paths: + path = Path(lib_path) + if not path.is_dir(): + invalid_paths.append((path, "Directory does not exist")) + continue + + sym_files = list(path.glob("*.kicad_sym")) + if not sym_files: + invalid_paths.append((path, "No .kicad_sym symbol files found")) + continue + + valid_paths.append(path) + lib_search_paths[KICAD].append(str(path)) + + if valid_paths: + logger.info("Added KiCad library paths:") + for path in valid_paths: + logger.info(f" ✓ {path}") + sym_files = list(path.glob("*.kicad_sym")) + for sym in sym_files[:3]: + logger.info(f" - {sym.name}") + if len(sym_files) > 3: + logger.info(f" - ... and {len(sym_files)-3} more") -def main(): + if invalid_paths: + logger.warning("Invalid library paths:") + for path, reason in invalid_paths: + logger.warning(f" ✗ {path}: {reason}") + +def generate_netlist(args, output_dir: Path) -> Optional[Path]: + """Generate netlist from schematic if requested.""" + if not (args.schematic and args.generate_netlist): + return None + + logger.info("Step 1: Generating netlist from schematic...") + schematic_path = Path(args.schematic) + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic not found: {schematic_path}") + + kicad_cli = validate_kicad_cli(args.kicad_cli) + netlist_path = output_dir / f"{schematic_path.stem}.net" + + try: + subprocess.run([ + kicad_cli, 'sch', 'export', 'netlist', + '-o', str(netlist_path), str(schematic_path) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e + + logger.info(f"✓ Generated netlist: {netlist_path}") + return netlist_path + +def generate_skidl_project(args, current_netlist: Optional[Path], output_dir: Path) -> Optional[Path]: + """Generate SKiDL project from netlist if requested.""" + if not args.generate_skidl or not current_netlist: + return None + + logger.info("Step 2: Generating SKiDL project from netlist...") + skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" + skidl_dir.mkdir(parents=True, exist_ok=True) + + try: + subprocess.run([ + 'netlist_to_skidl', + '-i', str(current_netlist), + '--output', str(skidl_dir) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e + + logger.info(f"✓ Generated SKiDL project: {skidl_dir}") + return skidl_dir + +def get_skidl_source(args, netlist_path: Optional[Path], skidl_dir: Optional[Path]) -> Path: + """Determine the SKiDL source to analyze.""" + if args.skidl: + source = Path(args.skidl) + if not source.exists(): + raise FileNotFoundError(f"SKiDL file not found: {source}") + logger.info(f"Using SKiDL file: {source}") + return source + elif args.skidl_dir: + source = Path(args.skidl_dir) + if not source.exists(): + raise FileNotFoundError(f"SKiDL directory not found: {source}") + logger.info(f"Using SKiDL project directory: {source}") + return source + elif skidl_dir: + return skidl_dir + else: + raise ValueError("No SKiDL source available for analysis") + +def main() -> None: """ Main execution function implementing the KiCad-SKiDL-LLM pipeline. - Supports: - 1. Direct analysis of SKiDL Python files - 2. Analysis of SKiDL project directories - 3. KiCad schematic conversion and analysis + The pipeline supports: + 1. KiCad schematic to netlist conversion + 2. Netlist to SKiDL project generation + 3. Circuit analysis using LLM """ start_time = time.time() try: logger.info("Starting KiCad-SKiDL-LLM pipeline") args = parse_args() - # Determine output directory - t0 = time.time() - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Created output directory: {output_dir}") - # Track the SKiDL source to analyze - skidl_source = None + # Setup environment + output_dir = setup_environment(args) + + # Add KiCad library paths if provided + if args.kicad_lib_paths: + handle_kicad_libraries(args.kicad_lib_paths) + + # Process input source and generate required files + netlist_path = None + skidl_dir = None - # Handle KiCad conversion if requested if args.schematic or args.netlist: - current_netlist = None - - # Step 1: Generate netlist if requested - if args.schematic and args.generate_netlist: - logger.info("Step 1: Generating netlist from schematic...") - schematic_path = Path(args.schematic) - if not schematic_path.exists(): - raise FileNotFoundError(f"Schematic not found: {schematic_path}") - - # Validate KiCad CLI - kicad_cli = validate_kicad_cli(args.kicad_cli) - - # Generate netlist - netlist_path = output_dir / f"{schematic_path.stem}.net" - try: - subprocess.run([ - kicad_cli, 'sch', 'export', 'netlist', - '-o', str(netlist_path), str(schematic_path) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e - - current_netlist = netlist_path - logger.info(f"✓ Generated netlist: {netlist_path}") - - elif args.netlist: - current_netlist = Path(args.netlist) - if not current_netlist.exists(): - raise FileNotFoundError(f"Netlist not found: {current_netlist}") - logger.info(f"Using existing netlist: {current_netlist}") + # Handle netlist + if args.netlist: + netlist_path = Path(args.netlist) + if not netlist_path.exists(): + raise FileNotFoundError(f"Netlist not found: {netlist_path}") + logger.info(f"Using existing netlist: {netlist_path}") + else: + netlist_path = generate_netlist(args, output_dir) - # Step 2: Generate SKiDL if requested + # Generate SKiDL project if requested if args.generate_skidl: - logger.info("Step 2: Generating SKiDL project from netlist...") - skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" - skidl_dir.mkdir(parents=True, exist_ok=True) - - try: - subprocess.run([ - 'netlist_to_skidl', - '-i', str(current_netlist), - '--output', str(skidl_dir) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e - - skidl_source = skidl_dir - logger.info(f"✓ Generated SKiDL project: {skidl_dir}") + skidl_dir = generate_skidl_project(args, netlist_path, output_dir) - # Use direct SKiDL source if provided - elif args.skidl: - skidl_source = Path(args.skidl) - if not skidl_source.exists(): - raise FileNotFoundError(f"SKiDL file not found: {skidl_source}") - logger.info(f"Using SKiDL file: {skidl_source}") + # Run circuit analysis if requested + if args.analyze: + skidl_source = get_skidl_source(args, netlist_path, skidl_dir) - elif args.skidl_dir: - skidl_source = Path(args.skidl_dir) - if not skidl_source.exists(): - raise FileNotFoundError(f"SKiDL directory not found: {skidl_source}") - logger.info(f"Using SKiDL project directory: {skidl_source}") - - # Add KiCad library paths if provided - if args.kicad_lib_paths: - from skidl import lib_search_paths, KICAD - valid_paths = [] - invalid_paths = [] + # Parse skip circuits if provided + skip_circuits = set() + if args.skip_circuits: + skip_circuits = {c.strip() for c in args.skip_circuits.split(',')} - for lib_path in args.kicad_lib_paths: - path = Path(lib_path) - if not path.is_dir(): - invalid_paths.append((path, "Directory does not exist")) - continue - - # Check for KiCad 8 symbol files (.kicad_sym) - sym_files = list(path.glob("*.kicad_sym")) - if not sym_files: - invalid_paths.append((path, "No .kicad_sym symbol files found")) - continue - - valid_paths.append(path) - lib_search_paths[KICAD].append(str(path)) - - # Log results - if valid_paths: - logger.info("Added KiCad 8 library paths:") - for path in valid_paths: - logger.info(f" ✓ {path}") - sym_files = list(path.glob("*.kicad_sym")) - for sym in sym_files[:3]: # Show up to 3 symbol libraries - logger.info(f" - {sym.name}") - if len(sym_files) > 3: - logger.info(f" - ... and {len(sym_files)-3} more") - - if invalid_paths: - logger.warning("Some library paths were invalid:") - for path, reason in invalid_paths: - logger.warning(f" ✗ {path}: {reason}") - - # Step 3: Run analysis if requested - if args.analyze: - if not skidl_source: - raise ValueError("No SKiDL source available for analysis") - - logger.info("Step 3: Analyzing circuits...") try: - results = CircuitAnalyzer.analyze_circuits( + results = analyze_circuits( source=skidl_source, output_file=args.analysis_output, api_key=args.api_key, - backend=args.backend, + backend=Backend(args.backend), model=args.model, prompt=args.analysis_prompt, - dump_temp=args.dump_temp + skip_circuits=skip_circuits ) if results["success"]: logger.info("Analysis Results:") - # Handle subcircuit results if present if "subcircuits" in results: for hier, analysis in results["subcircuits"].items(): logger.info(f"\nSubcircuit: {hier}") if analysis["success"]: logger.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - tokens = analysis.get('prompt_tokens', 0) + analysis.get('response_tokens', 0) + tokens = analysis.get('prompt_tokens', 0) + analysis.get('completion_tokens', 0) if tokens: logger.info(f" Tokens used: {tokens}") else: @@ -591,9 +622,8 @@ def main(): if results.get('total_tokens'): logger.info(f"Total tokens used: {results['total_tokens']}") else: - # Single circuit analysis results logger.info(f"✓ Analysis completed in {results['request_time_seconds']:.2f} seconds") - tokens = results.get('prompt_tokens', 0) + results.get('response_tokens', 0) + tokens = results.get('prompt_tokens', 0) + results.get('completion_tokens', 0) if tokens: logger.info(f"Tokens used: {tokens}") @@ -614,6 +644,9 @@ def main(): logger.error("1. Verify Ollama is running locally") logger.error("2. Check if the requested model is installed") raise + + total_time = time.time() - start_time + logger.info(f"Pipeline completed in {total_time:.2f} seconds") except Exception as e: logger.error(f"Pipeline failed: {str(e)}") From 696c849b0b9f18009c01d06754768e94d05c7f59 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 08:05:22 -0800 Subject: [PATCH 65/73] refactor shared logic --- kicad_skidl_llm.py | 340 ++++++++++++++++++--------------------------- 1 file changed, 135 insertions(+), 205 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index c5aa4fe6e..42bb9fd66 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -71,6 +71,94 @@ class CircuitDiscoveryError(Exception): pass +def _analyze_subcircuits( + hierarchies: Set[str], + output_file: Optional[Path], + api_key: Optional[str], + backend: Backend, + model: Optional[str], + prompt: Optional[str], + start_time: float +) -> Dict[str, Union[bool, float, int, Dict]]: + """ + Analyze subcircuits using LLM and generate consolidated results. + + Args: + hierarchies: Set of circuit hierarchies to analyze + output_file: Output file for analysis results + api_key: API key for LLM service + backend: LLM backend to use + model: Model name for selected backend + prompt: Custom analysis prompt + start_time: Start time of analysis for timing calculations + + Returns: + Dictionary containing analysis results and metadata + """ + if not hierarchies: + logger.warning("No valid circuits found for analysis after applying skip filters") + return { + "success": True, + "subcircuits": {}, + "total_time_seconds": time.time() - start_time, + "total_tokens": 0 + } + + results = { + "success": True, + "subcircuits": {}, + "total_time_seconds": 0, + "total_tokens": 0 + } + + consolidated_text = ["=== Subcircuits Analysis ===\n"] + for hier in sorted(hierarchies): + logger.info(f"\nAnalyzing subcircuit: {hier}") + + # Get description focused on this subcircuit + circuit_desc = default_circuit.get_circuit_info(hierarchy=hier, depth=1) + + # Analyze just this subcircuit + sub_results = default_circuit.analyze_with_llm( + api_key=api_key, + output_file=None, # Don't write individual files + backend=backend.value, + model=model or DEFAULT_MODEL, + custom_prompt=prompt, + analyze_subcircuits=False # Only analyze this specific subcircuit + ) + + results["subcircuits"][hier] = sub_results + results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) + results["total_tokens"] += ( + sub_results.get("prompt_tokens", 0) + + sub_results.get("completion_tokens", 0) + ) + + # Build consolidated output text + consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") + if sub_results.get("success", False): + analysis_text = sub_results.get("analysis", "No analysis available") + consolidated_text.append(analysis_text) + + token_info = ( + f"\nTokens used: {sub_results.get('total_tokens', 0)} " + f"(Prompt: {sub_results.get('prompt_tokens', 0)}, " + f"Completion: {sub_results.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + else: + consolidated_text.append( + f"Analysis failed: {sub_results.get('error', 'Unknown error')}" + ) + consolidated_text.append("\n") + + # Save consolidated results + if output_file: + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + return results def analyze_circuits( source: Path, @@ -82,7 +170,8 @@ def analyze_circuits( skip_circuits: Optional[Set[str]] = None, dump_temp: bool = False ) -> Dict[str, Union[bool, float, int, Dict]]: - """Analyze SKiDL circuits using the specified LLM backend. + """ + Analyze SKiDL circuits using the specified LLM backend. Args: source: Path to SKiDL file or directory @@ -93,6 +182,9 @@ def analyze_circuits( prompt: Custom analysis prompt skip_circuits: Set of circuit hierarchies to skip dump_temp: Flag to dump temporary files + + Returns: + Dictionary containing analysis results and metadata """ start_time = time.time() skip_circuits = skip_circuits or set() @@ -100,128 +192,18 @@ def analyze_circuits( if skip_circuits: logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") - if source.is_file(): - logger.info(f"Starting analysis of SKiDL file: {source}") - - sys.path.insert(0, str(source.parent)) - try: - # Import phase - t0 = time.time() - logger.info("Importing SKiDL module...") - module = importlib.import_module(source.stem) - importlib.reload(module) - logger.info(f"Import completed in {time.time() - t0:.2f} seconds") - - # Circuit instantiation phase - if hasattr(module, 'main'): - t0 = time.time() - logger.info("Executing circuit main()...") - module.main() - logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") - - # LLM Analysis phase - t0 = time.time() - logger.info("Starting LLM analysis...") - - # Get hierarchies from parts and filter out skipped ones - hierarchies = set() - for part in default_circuit.parts: - if part.hierarchy != default_circuit.hierarchy: # Skip top level - if part.hierarchy not in skip_circuits: - hierarchies.add(part.hierarchy) - - if not hierarchies: - logger.warning("No valid circuits found for analysis after applying skip filters") - return { - "success": True, - "subcircuits": {}, - "total_time_seconds": time.time() - start_time, - "total_tokens": 0 - } - - # Generate consolidated results - results = { - "success": True, - "subcircuits": {}, - "total_time_seconds": 0, - "total_tokens": 0 - } - - # Analyze each non-skipped subcircuit - consolidated_text = ["=== Subcircuits Analysis ===\n"] - for hier in sorted(hierarchies): - logger.info(f"\nAnalyzing subcircuit: {hier}") - - # Get description focused on this subcircuit - circuit_desc = default_circuit.get_circuit_info(hierarchy=hier, depth=1) - - # Analyze just this subcircuit - sub_results = default_circuit.analyze_with_llm( - api_key=api_key, - output_file=None, # Don't write individual files - backend=backend.value, - model=model or DEFAULT_MODEL, - custom_prompt=prompt, - analyze_subcircuits=False # Only analyze this specific subcircuit - ) - - results["subcircuits"][hier] = sub_results - results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) - results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("completion_tokens", 0) - - # Build consolidated output text - consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") - if sub_results.get("success", False): - analysis_text = sub_results.get("analysis", "No analysis available") - consolidated_text.append(analysis_text) - - token_info = ( - f"\nTokens used: {sub_results.get('total_tokens', 0)} " - f"(Prompt: {sub_results.get('prompt_tokens', 0)}, " - f"Completion: {sub_results.get('completion_tokens', 0)})" - ) - consolidated_text.append(token_info) - else: - consolidated_text.append( - f"Analysis failed: {sub_results.get('error', 'Unknown error')}" - ) - consolidated_text.append("\n") - - # Save consolidated results - if output_file: - with open(output_file, "w") as f: - f.write("\n".join(consolidated_text)) - - logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") - total_time = time.time() - start_time - logger.info(f"Total processing time: {total_time:.2f} seconds") - - return results - - except Exception as e: - logger.error(f"Analysis failed: {str(e)}") - raise - finally: - sys.path.pop(0) - - else: - # Directory case - import main.py from converted project - logger.info(f"Starting analysis of KiCad conversion directory: {source}") + sys.path.insert(0, str(source.parent if source.is_file() else source)) + try: + # Import phase + t0 = time.time() + module_name = source.stem if source.is_file() else 'main' + logger.info(f"Importing {module_name} module...") + module = importlib.import_module(module_name) + importlib.reload(module) + logger.info(f"Import completed in {time.time() - t0:.2f} seconds") - main_file = source / 'main.py' - if not main_file.exists(): - raise CircuitDiscoveryError(f"No main.py found in {source}") - - sys.path.insert(0, str(source)) - try: - # Import phase - t0 = time.time() - logger.info("Importing project main module...") - module = importlib.import_module('main') - importlib.reload(module) - logger.info(f"Import completed in {time.time() - t0:.2f} seconds") - - # Circuit instantiation phase + # Circuit instantiation phase + if hasattr(module, 'main'): t0 = time.time() logger.info("Executing circuit main()...") try: @@ -236,89 +218,37 @@ def analyze_circuits( raise CircuitDiscoveryError(msg) from e raise logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") - - # LLM Analysis phase - t0 = time.time() - logger.info("Starting LLM analysis...") - - # Get hierarchies and filter out skipped ones - hierarchies = set() - for part in default_circuit.parts: - if part.hierarchy != default_circuit.hierarchy: # Skip top level - if part.hierarchy not in skip_circuits: - hierarchies.add(part.hierarchy) - - if not hierarchies: - logger.warning("No valid circuits found for analysis after applying skip filters") - return { - "success": True, - "subcircuits": {}, - "total_time_seconds": time.time() - start_time, - "total_tokens": 0 - } - - # Generate consolidated results - results = { - "success": True, - "subcircuits": {}, - "total_time_seconds": 0, - "total_tokens": 0 - } - - # Analyze each non-skipped subcircuit - consolidated_text = ["=== Subcircuits Analysis ===\n"] - for hier in sorted(hierarchies): - logger.info(f"\nAnalyzing subcircuit: {hier}") - - # Get description focused on this subcircuit - circuit_desc = default_circuit.get_circuit_info(hierarchy=hier, depth=1) - - # Analyze just this subcircuit - sub_results = default_circuit.analyze_with_llm( - api_key=api_key, - output_file=None, # Don't write individual files - backend=backend.value, - model=model or DEFAULT_MODEL, - custom_prompt=prompt, - analyze_subcircuits=False # Only analyze this specific subcircuit - ) - - results["subcircuits"][hier] = sub_results - results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) - results["total_tokens"] += sub_results.get("prompt_tokens", 0) + sub_results.get("completion_tokens", 0) - - # Build consolidated output text - consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") - if sub_results.get("success", False): - analysis_text = sub_results.get("analysis", "No analysis available") - consolidated_text.append(analysis_text) - - token_info = ( - f"\nTokens used: {sub_results.get('total_tokens', 0)} " - f"(Prompt: {sub_results.get('prompt_tokens', 0)}, " - f"Completion: {sub_results.get('completion_tokens', 0)})" - ) - consolidated_text.append(token_info) - else: - consolidated_text.append( - f"Analysis failed: {sub_results.get('error', 'Unknown error')}" - ) - consolidated_text.append("\n") - - # Save consolidated results - if output_file: - with open(output_file, "w") as f: - f.write("\n".join(consolidated_text)) - - logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") - total_time = time.time() - start_time - logger.info(f"Total processing time: {total_time:.2f} seconds") - - return results - - finally: - sys.path.pop(0) - + + # LLM Analysis phase + t0 = time.time() + logger.info("Starting LLM analysis...") + + # Get hierarchies and filter out skipped ones + hierarchies = set() + for part in default_circuit.parts: + if part.hierarchy != default_circuit.hierarchy: # Skip top level + if part.hierarchy not in skip_circuits: + hierarchies.add(part.hierarchy) + + results = _analyze_subcircuits( + hierarchies=hierarchies, + output_file=output_file, + api_key=api_key, + backend=backend, + model=model, + prompt=prompt, + start_time=start_time + ) + + logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") + total_time = time.time() - start_time + logger.info(f"Total processing time: {total_time:.2f} seconds") + + return results + + finally: + sys.path.pop(0) + def validate_kicad_cli(path: str) -> str: """ Validate KiCad CLI executable and provide platform-specific guidance. From 18b0173b627333fb9e7d9ca43b2450faded419b9 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 08:32:47 -0800 Subject: [PATCH 66/73] make analysis llm calls parallel --- kicad_skidl_llm.py | 337 ++++++++++++++++++++++++++------------------- 1 file changed, 196 insertions(+), 141 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index 42bb9fd66..6ff13eba5 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -12,16 +12,19 @@ - Circuit analysis using OpenRouter or Ollama LLM backends - Support for subcircuit analysis with selective circuit skipping - Detailed logging and timing information + - Parallel analysis of subcircuits Example Usage: # Analyze KiCad schematic - python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist --generate-skidl --analyze --api-key YOUR_KEY - + python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist \ + --generate-skidl --analyze --api-key YOUR_KEY + # Analyze existing SKiDL project using Ollama python kicad_skidl_llm.py --skidl-dir project/ --analyze --backend ollama - + # Skip specific circuits during analysis - python kicad_skidl_llm.py --skidl circuit.py --analyze --skip-circuits "voltage_regulator1,adc_interface2" + python kicad_skidl_llm.py --skidl circuit.py --analyze \ + --skip-circuits "voltage_regulator1,adc_interface2" """ import os @@ -30,21 +33,24 @@ import subprocess import importlib import textwrap -import traceback from pathlib import Path -from typing import List, Dict, Optional, Union, Tuple, Set +from typing import List, Dict, Optional, Union, Set import argparse import logging from datetime import datetime import time from enum import Enum +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass, field +import json from skidl import * # Global configuration -DEFAULT_TIMEOUT = 300 -DEFAULT_MODEL = "google/gemini-2.0-flash-001" -DEFAULT_OUTPUT_DIR = Path.cwd() / "output" +DEFAULT_TIMEOUT = 300 # Maximum time to wait for LLM response +DEFAULT_MODEL = "google/gemini-2.0-flash-001" # Default LLM model +DEFAULT_OUTPUT_DIR = Path.cwd() / "output" # Default output directory class Backend(Enum): """Supported LLM backends.""" @@ -70,95 +76,103 @@ class CircuitDiscoveryError(Exception): """Raised when there are issues discovering or loading circuits.""" pass +@dataclass +class AnalysisState: + """ + Tracks the state of circuit analysis across threads. + + This class maintains thread-safe state information about ongoing circuit analyses, + including completed circuits, failures, and timing metrics. + """ + completed: Set[str] = field(default_factory=set) + failed: Dict[str, str] = field(default_factory=dict) # circuit -> error message + results: Dict[str, Dict] = field(default_factory=dict) + lock: threading.Lock = field(default_factory=threading.Lock) + total_analysis_time: float = 0.0 + + def save_state(self, path: Path): + """Save current analysis state to disk.""" + with self.lock: + state_dict = { + "completed": list(self.completed), + "failed": self.failed, + "results": self.results, + "total_analysis_time": self.total_analysis_time + } + with open(path, 'w') as f: + json.dump(state_dict, f, indent=2) + + @classmethod + def load_state(cls, path: Path) -> 'AnalysisState': + """Load analysis state from disk.""" + with open(path) as f: + state_dict = json.load(f) + state = cls() + state.completed = set(state_dict["completed"]) + state.failed = state_dict["failed"] + state.results = state_dict["results"] + state.total_analysis_time = state_dict.get("total_analysis_time", 0.0) + return state + + def add_result(self, circuit: str, result: Dict): + """Thread-safe addition of analysis result.""" + with self.lock: + self.results[circuit] = result + self.completed.add(circuit) + # Add the analysis time from this result + if "request_time_seconds" in result: + self.total_analysis_time += result["request_time_seconds"] + + def add_failure(self, circuit: str, error: str): + """Thread-safe recording of analysis failure.""" + with self.lock: + self.failed[circuit] = error -def _analyze_subcircuits( - hierarchies: Set[str], - output_file: Optional[Path], +def analyze_single_circuit( + circuit: str, api_key: Optional[str], backend: Backend, model: Optional[str], prompt: Optional[str], - start_time: float -) -> Dict[str, Union[bool, float, int, Dict]]: + state: AnalysisState +) -> None: """ - Analyze subcircuits using LLM and generate consolidated results. + Analyze a single circuit and update the shared state. Args: - hierarchies: Set of circuit hierarchies to analyze - output_file: Output file for analysis results + circuit: Hierarchy name of circuit to analyze api_key: API key for LLM service backend: LLM backend to use model: Model name for selected backend prompt: Custom analysis prompt - start_time: Start time of analysis for timing calculations - - Returns: - Dictionary containing analysis results and metadata + state: Shared analysis state object """ - if not hierarchies: - logger.warning("No valid circuits found for analysis after applying skip filters") - return { - "success": True, - "subcircuits": {}, - "total_time_seconds": time.time() - start_time, - "total_tokens": 0 - } - - results = { - "success": True, - "subcircuits": {}, - "total_time_seconds": 0, - "total_tokens": 0 - } - - consolidated_text = ["=== Subcircuits Analysis ===\n"] - for hier in sorted(hierarchies): - logger.info(f"\nAnalyzing subcircuit: {hier}") + try: + start_time = time.time() - # Get description focused on this subcircuit - circuit_desc = default_circuit.get_circuit_info(hierarchy=hier, depth=1) + # Get description focused on this circuit + circuit_desc = default_circuit.get_circuit_info(hierarchy=circuit, depth=1) - # Analyze just this subcircuit - sub_results = default_circuit.analyze_with_llm( + # Analyze just this circuit + result = default_circuit.analyze_with_llm( api_key=api_key, - output_file=None, # Don't write individual files + output_file=None, backend=backend.value, model=model or DEFAULT_MODEL, custom_prompt=prompt, - analyze_subcircuits=False # Only analyze this specific subcircuit + analyze_subcircuits=False ) - results["subcircuits"][hier] = sub_results - results["total_time_seconds"] += sub_results.get("request_time_seconds", 0) - results["total_tokens"] += ( - sub_results.get("prompt_tokens", 0) + - sub_results.get("completion_tokens", 0) - ) + # Add timing information + result["request_time_seconds"] = time.time() - start_time - # Build consolidated output text - consolidated_text.append(f"\n{'='*20} {hier} {'='*20}\n") - if sub_results.get("success", False): - analysis_text = sub_results.get("analysis", "No analysis available") - consolidated_text.append(analysis_text) - - token_info = ( - f"\nTokens used: {sub_results.get('total_tokens', 0)} " - f"(Prompt: {sub_results.get('prompt_tokens', 0)}, " - f"Completion: {sub_results.get('completion_tokens', 0)})" - ) - consolidated_text.append(token_info) - else: - consolidated_text.append( - f"Analysis failed: {sub_results.get('error', 'Unknown error')}" - ) - consolidated_text.append("\n") - - # Save consolidated results - if output_file: - with open(output_file, "w") as f: - f.write("\n".join(consolidated_text)) - - return results + state.add_result(circuit, result) + logger.info(f"✓ Completed analysis of {circuit}") + + except Exception as e: + error_msg = f"Analysis failed: {str(e)}" + state.add_failure(circuit, error_msg) + logger.error(f"✗ Failed analysis of {circuit}: {str(e)}") def analyze_circuits( source: Path, @@ -168,10 +182,11 @@ def analyze_circuits( model: Optional[str] = None, prompt: Optional[str] = None, skip_circuits: Optional[Set[str]] = None, - dump_temp: bool = False + max_concurrent: int = 4, + state_file: Optional[Path] = None ) -> Dict[str, Union[bool, float, int, Dict]]: """ - Analyze SKiDL circuits using the specified LLM backend. + Analyze SKiDL circuits using parallel LLM analysis. Args: source: Path to SKiDL file or directory @@ -181,74 +196,118 @@ def analyze_circuits( model: Model name for selected backend prompt: Custom analysis prompt skip_circuits: Set of circuit hierarchies to skip - dump_temp: Flag to dump temporary files + max_concurrent: Maximum number of concurrent analyses + state_file: Optional path to save/load analysis state Returns: Dictionary containing analysis results and metadata """ - start_time = time.time() + pipeline_start_time = time.time() skip_circuits = skip_circuits or set() - + + # Load previous state if it exists + state = AnalysisState.load_state(state_file) if state_file and state_file.exists() else AnalysisState() + if skip_circuits: logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") sys.path.insert(0, str(source.parent if source.is_file() else source)) try: - # Import phase - t0 = time.time() + # Import and setup phase module_name = source.stem if source.is_file() else 'main' logger.info(f"Importing {module_name} module...") module = importlib.import_module(module_name) importlib.reload(module) - logger.info(f"Import completed in {time.time() - t0:.2f} seconds") - # Circuit instantiation phase if hasattr(module, 'main'): - t0 = time.time() logger.info("Executing circuit main()...") - try: - module.main() - except FileNotFoundError as e: - if "Can't open file:" in str(e): - missing_lib = str(e).split(':')[-1].strip() - msg = ( - f"KiCad symbol library not found: {missing_lib}\n" - "Use --kicad-lib-paths to specify library directories" - ) - raise CircuitDiscoveryError(msg) from e - raise - logger.info(f"Circuit instantiation completed in {time.time() - t0:.2f} seconds") + module.main() - # LLM Analysis phase - t0 = time.time() - logger.info("Starting LLM analysis...") - - # Get hierarchies and filter out skipped ones + # Get circuits to analyze (excluding already completed ones) hierarchies = set() for part in default_circuit.parts: if part.hierarchy != default_circuit.hierarchy: # Skip top level - if part.hierarchy not in skip_circuits: + if part.hierarchy not in skip_circuits and part.hierarchy not in state.completed: hierarchies.add(part.hierarchy) - results = _analyze_subcircuits( - hierarchies=hierarchies, - output_file=output_file, - api_key=api_key, - backend=backend, - model=model, - prompt=prompt, - start_time=start_time - ) + if not hierarchies: + logger.info("No new circuits to analyze") + if state.completed: + logger.info(f"Previously analyzed: {len(state.completed)} circuits") + + else: + # Parallel analysis phase + logger.info(f"Starting parallel analysis of {len(hierarchies)} circuits...") + with ThreadPoolExecutor(max_workers=max_concurrent) as executor: + futures = [] + for circuit in sorted(hierarchies): + future = executor.submit( + analyze_single_circuit, + circuit=circuit, + api_key=api_key, + backend=backend, + model=model, + prompt=prompt, + state=state + ) + futures.append(future) + + # Wait for all analyses to complete + for future in as_completed(futures): + try: + future.result() # This will raise any exceptions from the thread + except Exception as e: + logger.error(f"Thread failed: {str(e)}") + + # Save intermediate state after each completion + if state_file: + state.save_state(state_file) - logger.info(f"LLM analysis completed in {time.time() - t0:.2f} seconds") - total_time = time.time() - start_time - logger.info(f"Total processing time: {total_time:.2f} seconds") + # Generate consolidated report + consolidated_text = ["=== Circuit Analysis Report ===\n"] + + if state.completed: + consolidated_text.append("\n=== Successful Analyses ===") + for circuit in sorted(state.completed): + result = state.results[circuit] + consolidated_text.append(f"\n{'='*20} {circuit} {'='*20}") + consolidated_text.append(result.get("analysis", "No analysis available")) + token_info = ( + f"\nTokens used: {result.get('total_tokens', 0)} " + f"(Prompt: {result.get('prompt_tokens', 0)}, " + f"Completion: {result.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + + if state.failed: + consolidated_text.append("\n=== Failed Analyses ===") + for circuit, error in sorted(state.failed.items()): + consolidated_text.append(f"\n{circuit}: {error}") - return results + # Save consolidated results + if output_file: + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + # Calculate total metrics + total_tokens = sum( + result.get("total_tokens", 0) + for result in state.results.values() + ) + + return { + "success": len(state.completed) > 0 and not state.failed, + "completed_circuits": sorted(state.completed), + "failed_circuits": state.failed, + "results": state.results, + "total_time_seconds": time.time() - pipeline_start_time, # Overall pipeline time + "total_analysis_time": state.total_analysis_time, # Cumulative analysis time + "total_tokens": total_tokens + } finally: sys.path.pop(0) - + def validate_kicad_cli(path: str) -> str: """ Validate KiCad CLI executable and provide platform-specific guidance. @@ -325,6 +384,8 @@ def parse_args() -> argparse.Namespace: %(prog)s --schematic design.kicad_sch --generate-netlist --generate-skidl --analyze --api-key YOUR_KEY # Analyze existing SKiDL project with circuit skipping + %(prog)s --skidl-dir project/ --analyze +# Skip specific circuits during analysis %(prog)s --skidl-dir project/ --analyze --skip-circuits "circuit1,circuit2" --api-key YOUR_KEY """) ) @@ -355,6 +416,8 @@ def parse_args() -> argparse.Namespace: parser.add_argument('--skip-circuits', help='Comma-separated list of circuits to skip during analysis') parser.add_argument('--kicad-lib-paths', nargs='*', help='List of custom KiCad library paths') + parser.add_argument('--max-concurrent', type=int, default=4, + help='Maximum number of concurrent LLM analyses (default: 4)') args = parser.parse_args() @@ -532,32 +595,24 @@ def main() -> None: backend=Backend(args.backend), model=args.model, prompt=args.analysis_prompt, - skip_circuits=skip_circuits + skip_circuits=skip_circuits, + max_concurrent=args.max_concurrent ) if results["success"]: - logger.info("Analysis Results:") - if "subcircuits" in results: - for hier, analysis in results["subcircuits"].items(): - logger.info(f"\nSubcircuit: {hier}") - if analysis["success"]: - logger.info(f"✓ Analysis completed in {analysis['request_time_seconds']:.2f} seconds") - tokens = analysis.get('prompt_tokens', 0) + analysis.get('completion_tokens', 0) - if tokens: - logger.info(f" Tokens used: {tokens}") - else: - logger.error(f"✗ Analysis failed: {analysis['error']}") - - logger.info(f"\nTotal analysis time: {results['total_time_seconds']:.2f} seconds") - if results.get('total_tokens'): - logger.info(f"Total tokens used: {results['total_tokens']}") - else: - logger.info(f"✓ Analysis completed in {results['request_time_seconds']:.2f} seconds") - tokens = results.get('prompt_tokens', 0) + results.get('completion_tokens', 0) - if tokens: - logger.info(f"Tokens used: {tokens}") + logger.info("\nAnalysis Results:") + logger.info(f" ✓ Completed Circuits: {len(results['completed_circuits'])}") + logger.info(f" ✓ Total Analysis Time: {results['total_analysis_time']:.2f} seconds") + logger.info(f" ✓ Total Pipeline Time: {results['total_time_seconds']:.2f} seconds") + if results.get('total_tokens'): + logger.info(f" ✓ Total Tokens Used: {results['total_tokens']}") + + if results["failed_circuits"]: + logger.warning("\nFailed Circuits:") + for circuit, error in results["failed_circuits"].items(): + logger.warning(f" ✗ {circuit}: {error}") - logger.info(f"Analysis results saved to: {args.analysis_output}") + logger.info(f"\nAnalysis results saved to: {args.analysis_output}") else: raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") @@ -576,7 +631,7 @@ def main() -> None: raise total_time = time.time() - start_time - logger.info(f"Pipeline completed in {total_time:.2f} seconds") + logger.info(f"\nPipeline completed in {total_time:.2f} seconds") except Exception as e: logger.error(f"Pipeline failed: {str(e)}") From c761172c167cbd190b5778e248474fa61144f61e Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 14:58:31 -0800 Subject: [PATCH 67/73] add required openai pip install, change logic to look for KICAD_SYMBOL_DIR environment variable --- kicad_skidl_llm.py | 66 +++++++++++++++++++++++++++++++++++----------- setup.py | 1 + 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py index 6ff13eba5..534512cd0 100644 --- a/kicad_skidl_llm.py +++ b/kicad_skidl_llm.py @@ -438,33 +438,56 @@ def setup_environment(args) -> Path: return output_dir def handle_kicad_libraries(lib_paths: Optional[List[str]]) -> None: - """Add and validate KiCad library paths.""" - if not lib_paths: - return - + """Add and validate KiCad library paths. + + Args: + lib_paths: List of paths to KiCad library directories + """ from skidl import lib_search_paths, KICAD valid_paths = [] invalid_paths = [] - for lib_path in lib_paths: - path = Path(lib_path) - if not path.is_dir(): - invalid_paths.append((path, "Directory does not exist")) - continue - - sym_files = list(path.glob("*.kicad_sym")) - if not sym_files: - invalid_paths.append((path, "No .kicad_sym symbol files found")) - continue + # First, clear any existing paths to avoid duplicates + lib_search_paths[KICAD] = [] + + # Add system KiCad library path from environment variable + system_lib_path = os.environ.get('KICAD_SYMBOL_DIR') + if system_lib_path: + path = Path(system_lib_path) + if path.is_dir(): + lib_search_paths[KICAD].append(str(path)) + valid_paths.append(path) + else: + logger.warning(f"KICAD_SYMBOL_DIR path does not exist: {path}") + else: + logger.warning("KICAD_SYMBOL_DIR environment variable not set") + logger.warning("Please set KICAD_SYMBOL_DIR to your KiCad symbols directory") + + # Then process any additional user-provided paths + if lib_paths: + for lib_path in lib_paths: + path = Path(lib_path) + if not path.is_dir(): + invalid_paths.append((path, "Directory does not exist")) + continue + + # Look for .kicad_sym files only + sym_files = list(path.glob("*.kicad_sym")) + if not sym_files: + invalid_paths.append((path, "No .kicad_sym files found")) + continue - valid_paths.append(path) - lib_search_paths[KICAD].append(str(path)) + valid_paths.append(path) + # Add the path to SKiDL's library search paths + if str(path) not in lib_search_paths[KICAD]: + lib_search_paths[KICAD].append(str(path)) if valid_paths: logger.info("Added KiCad library paths:") for path in valid_paths: logger.info(f" ✓ {path}") sym_files = list(path.glob("*.kicad_sym")) + # Show up to 3 symbol files as examples for sym in sym_files[:3]: logger.info(f" - {sym.name}") if len(sym_files) > 3: @@ -475,6 +498,17 @@ def handle_kicad_libraries(lib_paths: Optional[List[str]]) -> None: for path, reason in invalid_paths: logger.warning(f" ✗ {path}: {reason}") + if not valid_paths: + raise RuntimeError( + "No valid KiCad library paths found. Please ensure KICAD_SYMBOL_DIR " + "is set correctly and/or provide valid library paths." + ) + + # Debug output to verify paths are properly set + logger.debug("Final SKiDL library search paths:") + for path in lib_search_paths[KICAD]: + logger.debug(f" - {path}") + def generate_netlist(args, output_dir: Path) -> Optional[Path]: """Generate netlist from schematic if requested.""" if not (args.schematic and args.generate_netlist): diff --git a/setup.py b/setup.py index a2673219a..c9f555a14 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ "requests >= 2.31.0", "importlib-metadata", # For importlib support "typing-extensions", # For type hints in Python <3.8 + "openai", ] test_requirements = [ From 46d3a332bd9df25ac1d3edc4c8462d6081b2b096 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 15:28:29 -0800 Subject: [PATCH 68/73] moved kicad_skidl_llm script to folder --- setup.py | 3 +- src/skidl/scripts/kicad_skidl_llm_main.py | 606 ++++++++++++++++++++++ 2 files changed, 608 insertions(+), 1 deletion(-) create mode 100644 src/skidl/scripts/kicad_skidl_llm_main.py diff --git a/setup.py b/setup.py index c9f555a14..0dc675900 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,8 @@ packages=setuptools.find_packages(where="src"), entry_points={ "console_scripts": [ - "netlist_to_skidl = skidl.scripts.netlist_to_skidl_main:main" + "netlist_to_skidl = skidl.scripts.netlist_to_skidl_main:main", + "kicad_skidl_llm = skidl.scripts.kicad_skidl_llm_main:main" ] }, package_dir={"": "src"}, diff --git a/src/skidl/scripts/kicad_skidl_llm_main.py b/src/skidl/scripts/kicad_skidl_llm_main.py new file mode 100644 index 000000000..b4f964f73 --- /dev/null +++ b/src/skidl/scripts/kicad_skidl_llm_main.py @@ -0,0 +1,606 @@ +# -*- coding: utf-8 -*- +# The MIT License (MIT) - Copyright (c) Dave Vandenbout. + +""" +Command-line program for analyzing KiCad/SKiDL circuits using LLMs. +Supports direct SKiDL analysis, KiCad schematic conversion, and netlist processing. +""" + +import os +import sys +import platform +import subprocess +import importlib +import logging +from pathlib import Path +from typing import Optional, Set +import argparse +from datetime import datetime +import time +from enum import Enum +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed +from dataclasses import dataclass, field +import json + +from skidl import * +from skidl.pckg_info import __version__ + +# Constants +DEFAULT_TIMEOUT = 300 # Maximum time to wait for LLM response +DEFAULT_MODEL = "google/gemini-2.0-flash-001" # Default LLM model + +# Configure logging +logger = logging.getLogger("kicad_skidl_llm") + +class Backend(Enum): + """Supported LLM backends.""" + OPENROUTER = "openrouter" + OLLAMA = "ollama" + +@dataclass +class AnalysisState: + """Tracks the state of circuit analysis across threads.""" + completed: Set[str] = field(default_factory=set) + failed: dict = field(default_factory=dict) + results: dict = field(default_factory=dict) + lock: threading.Lock = field(default_factory=threading.Lock) + total_analysis_time: float = 0.0 + + def save_state(self, path: Path) -> None: + """Save current analysis state to disk.""" + with self.lock: + state_dict = { + "completed": list(self.completed), + "failed": self.failed, + "results": self.results, + "total_analysis_time": self.total_analysis_time + } + with open(path, 'w') as f: + json.dump(state_dict, f, indent=2) + + @classmethod + def load_state(cls, path: Path) -> 'AnalysisState': + """Load analysis state from disk.""" + with open(path) as f: + state_dict = json.load(f) + state = cls() + state.completed = set(state_dict["completed"]) + state.failed = state_dict["failed"] + state.results = state_dict["results"] + state.total_analysis_time = state_dict.get("total_analysis_time", 0.0) + return state + + def add_result(self, circuit: str, result: dict) -> None: + """Thread-safe addition of analysis result.""" + with self.lock: + self.results[circuit] = result + self.completed.add(circuit) + if "request_time_seconds" in result: + self.total_analysis_time += result["request_time_seconds"] + + def add_failure(self, circuit: str, error: str) -> None: + """Thread-safe recording of analysis failure.""" + with self.lock: + self.failed[circuit] = error + +def validate_kicad_cli(path: str) -> str: + """Validate KiCad CLI executable and provide platform-specific guidance.""" + system = platform.system().lower() + cli_path = Path(path) + + if not cli_path.exists(): + suggestions = { + 'darwin': [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "~/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + ], + 'windows': [ + r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\7.0\bin\kicad-cli.exe" + ], + 'linux': [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli" + ] + } + + error_msg = [f"KiCad CLI not found: {path}"] + if system in suggestions: + error_msg.append("\nCommon paths for your platform:") + for suggestion in suggestions[system]: + error_msg.append(f" - {suggestion}") + error_msg.append("\nSpecify the correct path using --kicad-cli") + raise FileNotFoundError('\n'.join(error_msg)) + + if not os.access(str(cli_path), os.X_OK): + if system == 'windows': + raise PermissionError( + f"KiCad CLI not executable: {path}\n" + "Ensure the file exists and you have appropriate permissions." + ) + else: + raise PermissionError( + f"KiCad CLI not executable: {path}\n" + f"Try making it executable with: chmod +x {path}" + ) + + return str(cli_path) + +def get_default_kicad_cli() -> str: + """Get the default KiCad CLI path based on the current platform.""" + system = platform.system().lower() + if system == 'darwin': # macOS + return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + elif system == 'windows': + return r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe" + else: # Linux and others + return "/usr/bin/kicad-cli" + +def handle_kicad_libraries(lib_paths: Optional[list]) -> None: + """Add and validate KiCad library paths.""" + from skidl import lib_search_paths, KICAD + valid_paths = [] + invalid_paths = [] + + # First, clear any existing paths to avoid duplicates + lib_search_paths[KICAD] = [] + + # Add system KiCad library path from environment variable + system_lib_path = os.environ.get('KICAD_SYMBOL_DIR') + if system_lib_path: + path = Path(system_lib_path) + if path.is_dir(): + lib_search_paths[KICAD].append(str(path)) + valid_paths.append(path) + else: + logger.warning(f"KICAD_SYMBOL_DIR path does not exist: {path}") + else: + logger.warning("KICAD_SYMBOL_DIR environment variable not set") + + # Process any additional user-provided paths + if lib_paths: + for lib_path in lib_paths: + path = Path(lib_path) + if not path.is_dir(): + invalid_paths.append((path, "Directory does not exist")) + continue + + sym_files = list(path.glob("*.kicad_sym")) + if not sym_files: + invalid_paths.append((path, "No .kicad_sym files found")) + continue + + valid_paths.append(path) + if str(path) not in lib_search_paths[KICAD]: + lib_search_paths[KICAD].append(str(path)) + + if not valid_paths: + raise RuntimeError( + "No valid KiCad library paths found. Please ensure KICAD_SYMBOL_DIR " + "is set correctly and/or provide valid library paths." + ) + +def generate_netlist(args, output_dir: Path) -> Optional[Path]: + """Generate netlist from schematic if requested.""" + if not (args.schematic and args.generate_netlist): + return None + + logger.info("Step 1: Generating netlist from schematic...") + schematic_path = Path(args.schematic) + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic not found: {schematic_path}") + + kicad_cli = validate_kicad_cli(args.kicad_cli) + netlist_path = output_dir / f"{schematic_path.stem}.net" + + try: + subprocess.run([ + kicad_cli, 'sch', 'export', 'netlist', + '-o', str(netlist_path), str(schematic_path) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e + + logger.info(f"✓ Generated netlist: {netlist_path}") + return netlist_path + +def generate_skidl_project(args, current_netlist: Optional[Path], output_dir: Path) -> Optional[Path]: + """Generate SKiDL project from netlist if requested.""" + if not args.generate_skidl or not current_netlist: + return None + + logger.info("Step 2: Generating SKiDL project from netlist...") + skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" + skidl_dir.mkdir(parents=True, exist_ok=True) + + try: + subprocess.run([ + 'netlist_to_skidl', + '-i', str(current_netlist), + '--output', str(skidl_dir) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e + + logger.info(f"✓ Generated SKiDL project: {skidl_dir}") + return skidl_dir + +def get_skidl_source(args, netlist_path: Optional[Path], skidl_dir: Optional[Path]) -> Path: + """Determine the SKiDL source to analyze.""" + if args.skidl: + source = Path(args.skidl) + if not source.exists(): + raise FileNotFoundError(f"SKiDL file not found: {source}") + return source + elif args.skidl_dir: + source = Path(args.skidl_dir) + if not source.exists(): + raise FileNotFoundError(f"SKiDL directory not found: {source}") + return source + elif skidl_dir: + return skidl_dir + else: + raise ValueError("No SKiDL source available for analysis") + +def analyze_single_circuit( + circuit: str, + api_key: Optional[str], + backend: Backend, + model: Optional[str], + prompt: Optional[str], + state: AnalysisState +) -> None: + """Analyze a single circuit and update the shared state.""" + try: + start_time = time.time() + + circuit_desc = default_circuit.get_circuit_info(hierarchy=circuit, depth=1) + + result = default_circuit.analyze_with_llm( + api_key=api_key, + output_file=None, + backend=backend.value, + model=model or DEFAULT_MODEL, + custom_prompt=prompt, + analyze_subcircuits=False + ) + + result["request_time_seconds"] = time.time() - start_time + + state.add_result(circuit, result) + logger.info(f"✓ Completed analysis of {circuit}") + + except Exception as e: + error_msg = f"Analysis failed: {str(e)}" + state.add_failure(circuit, error_msg) + logger.error(f"✗ Failed analysis of {circuit}: {str(e)}") + +def analyze_circuits( + source: Path, + output_file: Path, + api_key: Optional[str] = None, + backend: Backend = Backend.OPENROUTER, + model: Optional[str] = None, + prompt: Optional[str] = None, + skip_circuits: Optional[Set[str]] = None, + max_concurrent: int = 4, + state_file: Optional[Path] = None +) -> dict: + """Analyze SKiDL circuits using parallel LLM analysis.""" + pipeline_start_time = time.time() + skip_circuits = skip_circuits or set() + + state = AnalysisState.load_state(state_file) if state_file and state_file.exists() else AnalysisState() + + if skip_circuits: + logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") + + sys.path.insert(0, str(source.parent if source.is_file() else source)) + try: + module_name = source.stem if source.is_file() else 'main' + logger.info(f"Importing {module_name} module...") + module = importlib.import_module(module_name) + importlib.reload(module) + + if hasattr(module, 'main'): + logger.info("Executing circuit main()...") + module.main() + + hierarchies = set() + for part in default_circuit.parts: + if part.hierarchy != default_circuit.hierarchy: + if part.hierarchy not in skip_circuits and part.hierarchy not in state.completed: + hierarchies.add(part.hierarchy) + + if hierarchies: + logger.info(f"Starting parallel analysis of {len(hierarchies)} circuits...") + with ThreadPoolExecutor(max_workers=max_concurrent) as executor: + futures = [] + for circuit in sorted(hierarchies): + future = executor.submit( + analyze_single_circuit, + circuit=circuit, + api_key=api_key, + backend=backend, + model=model, + prompt=prompt, + state=state + ) + futures.append(future) + + for future in as_completed(futures): + try: + future.result() + except Exception as e: + logger.error(f"Thread failed: {str(e)}") + + if state_file: + state.save_state(state_file) + + consolidated_text = ["=== Circuit Analysis Report ===\n"] + + if state.completed: + consolidated_text.append("\n=== Successful Analyses ===") + for circuit in sorted(state.completed): + result = state.results[circuit] + consolidated_text.append(f"\n{'='*20} {circuit} {'='*20}\n") + consolidated_text.append(result.get("analysis", "No analysis available")) + token_info = ( + f"\nTokens used: {result.get('total_tokens', 0)} " + f"(Prompt: {result.get('prompt_tokens', 0)}, " + f"Completion: {result.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + + if state.failed: + consolidated_text.append("\n=== Failed Analyses ===") + for circuit, error in sorted(state.failed.items()): + consolidated_text.append(f"\n{circuit}: {error}") + + # Save consolidated results + if output_file: + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + # Calculate total metrics + total_tokens = sum( + result.get("total_tokens", 0) + for result in state.results.values() + ) + + return { + "success": len(state.completed) > 0 and not state.failed, + "completed_circuits": sorted(state.completed), + "failed_circuits": state.failed, + "results": state.results, + "total_time_seconds": time.time() - pipeline_start_time, + "total_analysis_time": state.total_analysis_time, + "total_tokens": total_tokens + } + + finally: + sys.path.pop(0) + +def main(): + """Main entry point for the KiCad-SKiDL-LLM pipeline.""" + parser = argparse.ArgumentParser( + description="A tool for analyzing KiCad/SKiDL circuits using LLMs.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", "-v", + action="version", + version="skidl " + __version__ + ) + + # Input source group (mutually exclusive) + source_group = parser.add_mutually_exclusive_group(required=True) + source_group.add_argument( + "--schematic", "-s", + help="Path to KiCad schematic (.kicad_sch) file" + ) + source_group.add_argument( + "--netlist", "-n", + help="Path to netlist (.net) file" + ) + source_group.add_argument( + "--skidl", + help="Path to SKiDL Python file to analyze" + ) + source_group.add_argument( + "--skidl-dir", + help="Path to SKiDL project directory" + ) + + # Operation mode flags + parser.add_argument( + "--generate-netlist", + action="store_true", + help="Generate netlist from schematic" + ) + parser.add_argument( + "--generate-skidl", + action="store_true", + help="Generate SKiDL project from netlist" + ) + parser.add_argument( + "--analyze", + action="store_true", + help="Run LLM analysis on circuits" + ) + + # Optional configuration + parser.add_argument( + "--kicad-cli", + default=get_default_kicad_cli(), + help="Path to kicad-cli executable" + ) + parser.add_argument( + "--output-dir", "-o", + default=".", + help="Output directory for generated files" + ) + parser.add_argument( + "--api-key", + help="OpenRouter API key for cloud LLM analysis" + ) + parser.add_argument( + "--backend", + choices=["openrouter", "ollama"], + default="openrouter", + help="LLM backend to use" + ) + parser.add_argument( + "--model", + help="LLM model name for selected backend" + ) + parser.add_argument( + "--analysis-output", + default="circuit_analysis.txt", + help="Output file for analysis results" + ) + parser.add_argument( + "--analysis-prompt", + help="Custom prompt for circuit analysis" + ) + parser.add_argument( + "--skip-circuits", + help="Comma-separated list of circuits to skip during analysis" + ) + parser.add_argument( + "--max-concurrent", + type=int, + default=4, + help="Maximum number of concurrent LLM analyses (default: 4)" + ) + parser.add_argument( + "--kicad-lib-paths", + nargs="*", + help="List of custom KiCad library paths" + ) + parser.add_argument( + "--debug", "-d", + nargs="?", + type=int, + default=0, + metavar="LEVEL", + help="Print debugging info. (Larger LEVEL means more info.)" + ) + + args = parser.parse_args() + + # Configure logging based on debug level + if args.debug is not None: + log_level = logging.DEBUG + 1 - args.debug + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter( + "[%(asctime)s] %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + )) + handler.setLevel(log_level) + logger.addHandler(handler) + logger.setLevel(log_level) + + # Validate argument combinations + if args.generate_netlist and not args.schematic: + logger.critical("--generate-netlist requires --schematic") + sys.exit(2) + if args.generate_skidl and not (args.netlist or args.generate_netlist): + logger.critical("--generate-skidl requires --netlist or --generate-netlist") + sys.exit(2) + if args.analyze and args.backend == "openrouter" and not args.api_key: + logger.critical("OpenRouter backend requires --api-key") + sys.exit(2) + + try: + start_time = time.time() + logger.info("Starting KiCad-SKiDL-LLM pipeline") + + # Setup environment + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Add KiCad library paths if provided + if args.kicad_lib_paths: + handle_kicad_libraries(args.kicad_lib_paths) + + # Process input source and generate required files + netlist_path = None + skidl_dir = None + + if args.schematic or args.netlist: + if args.netlist: + netlist_path = Path(args.netlist) + if not netlist_path.exists(): + raise FileNotFoundError(f"Netlist not found: {netlist_path}") + logger.info(f"Using existing netlist: {netlist_path}") + else: + netlist_path = generate_netlist(args, output_dir) + + if args.generate_skidl: + skidl_dir = generate_skidl_project(args, netlist_path, output_dir) + + # Run circuit analysis if requested + if args.analyze: + skidl_source = get_skidl_source(args, netlist_path, skidl_dir) + + # Parse skip circuits if provided + skip_circuits = set() + if args.skip_circuits: + skip_circuits = {c.strip() for c in args.skip_circuits.split(",")} + + try: + results = analyze_circuits( + source=skidl_source, + output_file=Path(args.analysis_output), + api_key=args.api_key, + backend=Backend(args.backend), + model=args.model, + prompt=args.analysis_prompt, + skip_circuits=skip_circuits, + max_concurrent=args.max_concurrent + ) + + if results["success"]: + logger.info("\nAnalysis Results:") + logger.info(f" ✓ Completed Circuits: {len(results['completed_circuits'])}") + logger.info(f" ✓ Total Analysis Time: {results['total_analysis_time']:.2f} seconds") + logger.info(f" ✓ Total Pipeline Time: {results['total_time_seconds']:.2f} seconds") + if results.get("total_tokens"): + logger.info(f" ✓ Total Tokens Used: {results['total_tokens']}") + + if results["failed_circuits"]: + logger.warning("\nFailed Circuits:") + for circuit, error in results["failed_circuits"].items(): + logger.warning(f" ✗ {circuit}: {error}") + + logger.info(f"\nAnalysis results saved to: {args.analysis_output}") + else: + raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") + + except Exception as e: + logger.error("✗ Circuit analysis failed!") + logger.error(f"Error: {str(e)}") + if args.backend == "openrouter": + logger.error("\nTroubleshooting tips:") + logger.error("1. Check your API key") + logger.error("2. Verify you have sufficient API credits") + logger.error("3. Check for rate limiting") + else: + logger.error("\nTroubleshooting tips:") + logger.error("1. Verify Ollama is running locally") + logger.error("2. Check if the requested model is installed") + sys.exit(1) + + total_time = time.time() - start_time + logger.info(f"\nPipeline completed in {total_time:.2f} seconds") + + except Exception as e: + logger.critical(f"Pipeline failed: {str(e)}") + logger.debug("Stack trace:", exc_info=True) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file From ba3852c11a46f84fcf3a6e50ca66d77c18dcfd7b Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 15:43:28 -0800 Subject: [PATCH 69/73] make smaller files for llm analysis feature --- src/skidl/scripts/kicad_skidl_llm_main.py | 597 +----------------- src/skidl/scripts/llm_analysis/__init__.py | 40 ++ src/skidl/scripts/llm_analysis/analyzer.py | 190 ++++++ src/skidl/scripts/llm_analysis/cli.py | 231 +++++++ src/skidl/scripts/llm_analysis/config.py | 25 + src/skidl/scripts/llm_analysis/generator.py | 115 ++++ src/skidl/scripts/llm_analysis/kicad.py | 129 ++++ src/skidl/scripts/llm_analysis/logging.py | 70 ++ .../scripts/llm_analysis/prompts/__init__.py | 14 + .../scripts/llm_analysis/prompts/base.py | 109 ++++ .../scripts/llm_analysis/prompts/sections.py | 133 ++++ src/skidl/scripts/llm_analysis/state.py | 115 ++++ 12 files changed, 1173 insertions(+), 595 deletions(-) create mode 100644 src/skidl/scripts/llm_analysis/__init__.py create mode 100644 src/skidl/scripts/llm_analysis/analyzer.py create mode 100644 src/skidl/scripts/llm_analysis/cli.py create mode 100644 src/skidl/scripts/llm_analysis/config.py create mode 100644 src/skidl/scripts/llm_analysis/generator.py create mode 100644 src/skidl/scripts/llm_analysis/kicad.py create mode 100644 src/skidl/scripts/llm_analysis/logging.py create mode 100644 src/skidl/scripts/llm_analysis/prompts/__init__.py create mode 100644 src/skidl/scripts/llm_analysis/prompts/base.py create mode 100644 src/skidl/scripts/llm_analysis/prompts/sections.py create mode 100644 src/skidl/scripts/llm_analysis/state.py diff --git a/src/skidl/scripts/kicad_skidl_llm_main.py b/src/skidl/scripts/kicad_skidl_llm_main.py index b4f964f73..141859dc7 100644 --- a/src/skidl/scripts/kicad_skidl_llm_main.py +++ b/src/skidl/scripts/kicad_skidl_llm_main.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # The MIT License (MIT) - Copyright (c) Dave Vandenbout. @@ -6,601 +7,7 @@ Supports direct SKiDL analysis, KiCad schematic conversion, and netlist processing. """ -import os -import sys -import platform -import subprocess -import importlib -import logging -from pathlib import Path -from typing import Optional, Set -import argparse -from datetime import datetime -import time -from enum import Enum -import threading -from concurrent.futures import ThreadPoolExecutor, as_completed -from dataclasses import dataclass, field -import json - -from skidl import * -from skidl.pckg_info import __version__ - -# Constants -DEFAULT_TIMEOUT = 300 # Maximum time to wait for LLM response -DEFAULT_MODEL = "google/gemini-2.0-flash-001" # Default LLM model - -# Configure logging -logger = logging.getLogger("kicad_skidl_llm") - -class Backend(Enum): - """Supported LLM backends.""" - OPENROUTER = "openrouter" - OLLAMA = "ollama" - -@dataclass -class AnalysisState: - """Tracks the state of circuit analysis across threads.""" - completed: Set[str] = field(default_factory=set) - failed: dict = field(default_factory=dict) - results: dict = field(default_factory=dict) - lock: threading.Lock = field(default_factory=threading.Lock) - total_analysis_time: float = 0.0 - - def save_state(self, path: Path) -> None: - """Save current analysis state to disk.""" - with self.lock: - state_dict = { - "completed": list(self.completed), - "failed": self.failed, - "results": self.results, - "total_analysis_time": self.total_analysis_time - } - with open(path, 'w') as f: - json.dump(state_dict, f, indent=2) - - @classmethod - def load_state(cls, path: Path) -> 'AnalysisState': - """Load analysis state from disk.""" - with open(path) as f: - state_dict = json.load(f) - state = cls() - state.completed = set(state_dict["completed"]) - state.failed = state_dict["failed"] - state.results = state_dict["results"] - state.total_analysis_time = state_dict.get("total_analysis_time", 0.0) - return state - - def add_result(self, circuit: str, result: dict) -> None: - """Thread-safe addition of analysis result.""" - with self.lock: - self.results[circuit] = result - self.completed.add(circuit) - if "request_time_seconds" in result: - self.total_analysis_time += result["request_time_seconds"] - - def add_failure(self, circuit: str, error: str) -> None: - """Thread-safe recording of analysis failure.""" - with self.lock: - self.failed[circuit] = error - -def validate_kicad_cli(path: str) -> str: - """Validate KiCad CLI executable and provide platform-specific guidance.""" - system = platform.system().lower() - cli_path = Path(path) - - if not cli_path.exists(): - suggestions = { - 'darwin': [ - "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", - "~/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" - ], - 'windows': [ - r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe", - r"C:\Program Files (x86)\KiCad\7.0\bin\kicad-cli.exe" - ], - 'linux': [ - "/usr/bin/kicad-cli", - "/usr/local/bin/kicad-cli" - ] - } - - error_msg = [f"KiCad CLI not found: {path}"] - if system in suggestions: - error_msg.append("\nCommon paths for your platform:") - for suggestion in suggestions[system]: - error_msg.append(f" - {suggestion}") - error_msg.append("\nSpecify the correct path using --kicad-cli") - raise FileNotFoundError('\n'.join(error_msg)) - - if not os.access(str(cli_path), os.X_OK): - if system == 'windows': - raise PermissionError( - f"KiCad CLI not executable: {path}\n" - "Ensure the file exists and you have appropriate permissions." - ) - else: - raise PermissionError( - f"KiCad CLI not executable: {path}\n" - f"Try making it executable with: chmod +x {path}" - ) - - return str(cli_path) - -def get_default_kicad_cli() -> str: - """Get the default KiCad CLI path based on the current platform.""" - system = platform.system().lower() - if system == 'darwin': # macOS - return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" - elif system == 'windows': - return r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe" - else: # Linux and others - return "/usr/bin/kicad-cli" - -def handle_kicad_libraries(lib_paths: Optional[list]) -> None: - """Add and validate KiCad library paths.""" - from skidl import lib_search_paths, KICAD - valid_paths = [] - invalid_paths = [] - - # First, clear any existing paths to avoid duplicates - lib_search_paths[KICAD] = [] - - # Add system KiCad library path from environment variable - system_lib_path = os.environ.get('KICAD_SYMBOL_DIR') - if system_lib_path: - path = Path(system_lib_path) - if path.is_dir(): - lib_search_paths[KICAD].append(str(path)) - valid_paths.append(path) - else: - logger.warning(f"KICAD_SYMBOL_DIR path does not exist: {path}") - else: - logger.warning("KICAD_SYMBOL_DIR environment variable not set") - - # Process any additional user-provided paths - if lib_paths: - for lib_path in lib_paths: - path = Path(lib_path) - if not path.is_dir(): - invalid_paths.append((path, "Directory does not exist")) - continue - - sym_files = list(path.glob("*.kicad_sym")) - if not sym_files: - invalid_paths.append((path, "No .kicad_sym files found")) - continue - - valid_paths.append(path) - if str(path) not in lib_search_paths[KICAD]: - lib_search_paths[KICAD].append(str(path)) - - if not valid_paths: - raise RuntimeError( - "No valid KiCad library paths found. Please ensure KICAD_SYMBOL_DIR " - "is set correctly and/or provide valid library paths." - ) - -def generate_netlist(args, output_dir: Path) -> Optional[Path]: - """Generate netlist from schematic if requested.""" - if not (args.schematic and args.generate_netlist): - return None - - logger.info("Step 1: Generating netlist from schematic...") - schematic_path = Path(args.schematic) - if not schematic_path.exists(): - raise FileNotFoundError(f"Schematic not found: {schematic_path}") - - kicad_cli = validate_kicad_cli(args.kicad_cli) - netlist_path = output_dir / f"{schematic_path.stem}.net" - - try: - subprocess.run([ - kicad_cli, 'sch', 'export', 'netlist', - '-o', str(netlist_path), str(schematic_path) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e - - logger.info(f"✓ Generated netlist: {netlist_path}") - return netlist_path - -def generate_skidl_project(args, current_netlist: Optional[Path], output_dir: Path) -> Optional[Path]: - """Generate SKiDL project from netlist if requested.""" - if not args.generate_skidl or not current_netlist: - return None - - logger.info("Step 2: Generating SKiDL project from netlist...") - skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" - skidl_dir.mkdir(parents=True, exist_ok=True) - - try: - subprocess.run([ - 'netlist_to_skidl', - '-i', str(current_netlist), - '--output', str(skidl_dir) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e - - logger.info(f"✓ Generated SKiDL project: {skidl_dir}") - return skidl_dir - -def get_skidl_source(args, netlist_path: Optional[Path], skidl_dir: Optional[Path]) -> Path: - """Determine the SKiDL source to analyze.""" - if args.skidl: - source = Path(args.skidl) - if not source.exists(): - raise FileNotFoundError(f"SKiDL file not found: {source}") - return source - elif args.skidl_dir: - source = Path(args.skidl_dir) - if not source.exists(): - raise FileNotFoundError(f"SKiDL directory not found: {source}") - return source - elif skidl_dir: - return skidl_dir - else: - raise ValueError("No SKiDL source available for analysis") - -def analyze_single_circuit( - circuit: str, - api_key: Optional[str], - backend: Backend, - model: Optional[str], - prompt: Optional[str], - state: AnalysisState -) -> None: - """Analyze a single circuit and update the shared state.""" - try: - start_time = time.time() - - circuit_desc = default_circuit.get_circuit_info(hierarchy=circuit, depth=1) - - result = default_circuit.analyze_with_llm( - api_key=api_key, - output_file=None, - backend=backend.value, - model=model or DEFAULT_MODEL, - custom_prompt=prompt, - analyze_subcircuits=False - ) - - result["request_time_seconds"] = time.time() - start_time - - state.add_result(circuit, result) - logger.info(f"✓ Completed analysis of {circuit}") - - except Exception as e: - error_msg = f"Analysis failed: {str(e)}" - state.add_failure(circuit, error_msg) - logger.error(f"✗ Failed analysis of {circuit}: {str(e)}") - -def analyze_circuits( - source: Path, - output_file: Path, - api_key: Optional[str] = None, - backend: Backend = Backend.OPENROUTER, - model: Optional[str] = None, - prompt: Optional[str] = None, - skip_circuits: Optional[Set[str]] = None, - max_concurrent: int = 4, - state_file: Optional[Path] = None -) -> dict: - """Analyze SKiDL circuits using parallel LLM analysis.""" - pipeline_start_time = time.time() - skip_circuits = skip_circuits or set() - - state = AnalysisState.load_state(state_file) if state_file and state_file.exists() else AnalysisState() - - if skip_circuits: - logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") - - sys.path.insert(0, str(source.parent if source.is_file() else source)) - try: - module_name = source.stem if source.is_file() else 'main' - logger.info(f"Importing {module_name} module...") - module = importlib.import_module(module_name) - importlib.reload(module) - - if hasattr(module, 'main'): - logger.info("Executing circuit main()...") - module.main() - - hierarchies = set() - for part in default_circuit.parts: - if part.hierarchy != default_circuit.hierarchy: - if part.hierarchy not in skip_circuits and part.hierarchy not in state.completed: - hierarchies.add(part.hierarchy) - - if hierarchies: - logger.info(f"Starting parallel analysis of {len(hierarchies)} circuits...") - with ThreadPoolExecutor(max_workers=max_concurrent) as executor: - futures = [] - for circuit in sorted(hierarchies): - future = executor.submit( - analyze_single_circuit, - circuit=circuit, - api_key=api_key, - backend=backend, - model=model, - prompt=prompt, - state=state - ) - futures.append(future) - - for future in as_completed(futures): - try: - future.result() - except Exception as e: - logger.error(f"Thread failed: {str(e)}") - - if state_file: - state.save_state(state_file) - - consolidated_text = ["=== Circuit Analysis Report ===\n"] - - if state.completed: - consolidated_text.append("\n=== Successful Analyses ===") - for circuit in sorted(state.completed): - result = state.results[circuit] - consolidated_text.append(f"\n{'='*20} {circuit} {'='*20}\n") - consolidated_text.append(result.get("analysis", "No analysis available")) - token_info = ( - f"\nTokens used: {result.get('total_tokens', 0)} " - f"(Prompt: {result.get('prompt_tokens', 0)}, " - f"Completion: {result.get('completion_tokens', 0)})" - ) - consolidated_text.append(token_info) - - if state.failed: - consolidated_text.append("\n=== Failed Analyses ===") - for circuit, error in sorted(state.failed.items()): - consolidated_text.append(f"\n{circuit}: {error}") - - # Save consolidated results - if output_file: - with open(output_file, "w") as f: - f.write("\n".join(consolidated_text)) - - # Calculate total metrics - total_tokens = sum( - result.get("total_tokens", 0) - for result in state.results.values() - ) - - return { - "success": len(state.completed) > 0 and not state.failed, - "completed_circuits": sorted(state.completed), - "failed_circuits": state.failed, - "results": state.results, - "total_time_seconds": time.time() - pipeline_start_time, - "total_analysis_time": state.total_analysis_time, - "total_tokens": total_tokens - } - - finally: - sys.path.pop(0) - -def main(): - """Main entry point for the KiCad-SKiDL-LLM pipeline.""" - parser = argparse.ArgumentParser( - description="A tool for analyzing KiCad/SKiDL circuits using LLMs.", - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - parser.add_argument( - "--version", "-v", - action="version", - version="skidl " + __version__ - ) - - # Input source group (mutually exclusive) - source_group = parser.add_mutually_exclusive_group(required=True) - source_group.add_argument( - "--schematic", "-s", - help="Path to KiCad schematic (.kicad_sch) file" - ) - source_group.add_argument( - "--netlist", "-n", - help="Path to netlist (.net) file" - ) - source_group.add_argument( - "--skidl", - help="Path to SKiDL Python file to analyze" - ) - source_group.add_argument( - "--skidl-dir", - help="Path to SKiDL project directory" - ) - - # Operation mode flags - parser.add_argument( - "--generate-netlist", - action="store_true", - help="Generate netlist from schematic" - ) - parser.add_argument( - "--generate-skidl", - action="store_true", - help="Generate SKiDL project from netlist" - ) - parser.add_argument( - "--analyze", - action="store_true", - help="Run LLM analysis on circuits" - ) - - # Optional configuration - parser.add_argument( - "--kicad-cli", - default=get_default_kicad_cli(), - help="Path to kicad-cli executable" - ) - parser.add_argument( - "--output-dir", "-o", - default=".", - help="Output directory for generated files" - ) - parser.add_argument( - "--api-key", - help="OpenRouter API key for cloud LLM analysis" - ) - parser.add_argument( - "--backend", - choices=["openrouter", "ollama"], - default="openrouter", - help="LLM backend to use" - ) - parser.add_argument( - "--model", - help="LLM model name for selected backend" - ) - parser.add_argument( - "--analysis-output", - default="circuit_analysis.txt", - help="Output file for analysis results" - ) - parser.add_argument( - "--analysis-prompt", - help="Custom prompt for circuit analysis" - ) - parser.add_argument( - "--skip-circuits", - help="Comma-separated list of circuits to skip during analysis" - ) - parser.add_argument( - "--max-concurrent", - type=int, - default=4, - help="Maximum number of concurrent LLM analyses (default: 4)" - ) - parser.add_argument( - "--kicad-lib-paths", - nargs="*", - help="List of custom KiCad library paths" - ) - parser.add_argument( - "--debug", "-d", - nargs="?", - type=int, - default=0, - metavar="LEVEL", - help="Print debugging info. (Larger LEVEL means more info.)" - ) - - args = parser.parse_args() - - # Configure logging based on debug level - if args.debug is not None: - log_level = logging.DEBUG + 1 - args.debug - handler = logging.StreamHandler(sys.stdout) - handler.setFormatter(logging.Formatter( - "[%(asctime)s] %(levelname)s: %(message)s", - datefmt="%Y-%m-%d %H:%M:%S" - )) - handler.setLevel(log_level) - logger.addHandler(handler) - logger.setLevel(log_level) - - # Validate argument combinations - if args.generate_netlist and not args.schematic: - logger.critical("--generate-netlist requires --schematic") - sys.exit(2) - if args.generate_skidl and not (args.netlist or args.generate_netlist): - logger.critical("--generate-skidl requires --netlist or --generate-netlist") - sys.exit(2) - if args.analyze and args.backend == "openrouter" and not args.api_key: - logger.critical("OpenRouter backend requires --api-key") - sys.exit(2) - - try: - start_time = time.time() - logger.info("Starting KiCad-SKiDL-LLM pipeline") - - # Setup environment - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - # Add KiCad library paths if provided - if args.kicad_lib_paths: - handle_kicad_libraries(args.kicad_lib_paths) - - # Process input source and generate required files - netlist_path = None - skidl_dir = None - - if args.schematic or args.netlist: - if args.netlist: - netlist_path = Path(args.netlist) - if not netlist_path.exists(): - raise FileNotFoundError(f"Netlist not found: {netlist_path}") - logger.info(f"Using existing netlist: {netlist_path}") - else: - netlist_path = generate_netlist(args, output_dir) - - if args.generate_skidl: - skidl_dir = generate_skidl_project(args, netlist_path, output_dir) - - # Run circuit analysis if requested - if args.analyze: - skidl_source = get_skidl_source(args, netlist_path, skidl_dir) - - # Parse skip circuits if provided - skip_circuits = set() - if args.skip_circuits: - skip_circuits = {c.strip() for c in args.skip_circuits.split(",")} - - try: - results = analyze_circuits( - source=skidl_source, - output_file=Path(args.analysis_output), - api_key=args.api_key, - backend=Backend(args.backend), - model=args.model, - prompt=args.analysis_prompt, - skip_circuits=skip_circuits, - max_concurrent=args.max_concurrent - ) - - if results["success"]: - logger.info("\nAnalysis Results:") - logger.info(f" ✓ Completed Circuits: {len(results['completed_circuits'])}") - logger.info(f" ✓ Total Analysis Time: {results['total_analysis_time']:.2f} seconds") - logger.info(f" ✓ Total Pipeline Time: {results['total_time_seconds']:.2f} seconds") - if results.get("total_tokens"): - logger.info(f" ✓ Total Tokens Used: {results['total_tokens']}") - - if results["failed_circuits"]: - logger.warning("\nFailed Circuits:") - for circuit, error in results["failed_circuits"].items(): - logger.warning(f" ✗ {circuit}: {error}") - - logger.info(f"\nAnalysis results saved to: {args.analysis_output}") - else: - raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") - - except Exception as e: - logger.error("✗ Circuit analysis failed!") - logger.error(f"Error: {str(e)}") - if args.backend == "openrouter": - logger.error("\nTroubleshooting tips:") - logger.error("1. Check your API key") - logger.error("2. Verify you have sufficient API credits") - logger.error("3. Check for rate limiting") - else: - logger.error("\nTroubleshooting tips:") - logger.error("1. Verify Ollama is running locally") - logger.error("2. Check if the requested model is installed") - sys.exit(1) - - total_time = time.time() - start_time - logger.info(f"\nPipeline completed in {total_time:.2f} seconds") - - except Exception as e: - logger.critical(f"Pipeline failed: {str(e)}") - logger.debug("Stack trace:", exc_info=True) - sys.exit(1) +from skidl.scripts.llm_analysis.cli import main if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/__init__.py b/src/skidl/scripts/llm_analysis/__init__.py new file mode 100644 index 000000000..0a44c3e1d --- /dev/null +++ b/src/skidl/scripts/llm_analysis/__init__.py @@ -0,0 +1,40 @@ +""" +Circuit analysis using Large Language Models for KiCad/SKiDL designs. + +This package provides tools for analyzing circuit designs using LLMs, +with support for: +- Direct SKiDL analysis +- KiCad schematic conversion +- Netlist processing +- Parallel circuit analysis +""" + +from .config import DEFAULT_TIMEOUT, DEFAULT_MODEL, Backend +from .state import AnalysisState +from .analyzer import analyze_circuits +from .generator import ( + generate_netlist, + generate_skidl_project, + get_skidl_source +) +from .kicad import ( + validate_kicad_cli, + get_default_kicad_cli, + handle_kicad_libraries +) + +__version__ = "1.0.0" + +__all__ = [ + "DEFAULT_TIMEOUT", + "DEFAULT_MODEL", + "Backend", + "AnalysisState", + "analyze_circuits", + "generate_netlist", + "generate_skidl_project", + "get_skidl_source", + "validate_kicad_cli", + "get_default_kicad_cli", + "handle_kicad_libraries", +] \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/analyzer.py b/src/skidl/scripts/llm_analysis/analyzer.py new file mode 100644 index 000000000..911f53aa2 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/analyzer.py @@ -0,0 +1,190 @@ +"""Core circuit analysis functionality using LLMs.""" + +import sys +import time +import logging +import importlib +from pathlib import Path +from typing import Optional, Set, Dict +from concurrent.futures import ThreadPoolExecutor, as_completed + +from skidl import * # Required for accessing default_circuit and other globals + +from .config import Backend, DEFAULT_MODEL +from .state import AnalysisState + +logger = logging.getLogger("kicad_skidl_llm") + +def analyze_single_circuit( + circuit: str, + api_key: Optional[str], + backend: Backend, + model: Optional[str], + prompt: Optional[str], + state: AnalysisState +) -> None: + """Analyze a single circuit and update the shared state. + + Args: + circuit: Circuit hierarchy path to analyze + api_key: API key for cloud LLM services + backend: LLM backend to use + model: Specific model to use for analysis + prompt: Custom analysis prompt + state: Shared analysis state tracker + """ + try: + start_time = time.time() + + circuit_desc = default_circuit.get_circuit_info(hierarchy=circuit, depth=1) + + result = default_circuit.analyze_with_llm( + api_key=api_key, + output_file=None, + backend=backend.value, + model=model or DEFAULT_MODEL, + custom_prompt=prompt, + analyze_subcircuits=False + ) + + result["request_time_seconds"] = time.time() - start_time + + state.add_result(circuit, result) + logger.info(f"✓ Completed analysis of {circuit}") + + except Exception as e: + error_msg = f"Analysis failed: {str(e)}" + state.add_failure(circuit, error_msg) + logger.error(f"✗ Failed analysis of {circuit}: {str(e)}") + +def analyze_circuits( + source: Path, + output_file: Path, + api_key: Optional[str] = None, + backend: Backend = Backend.OPENROUTER, + model: Optional[str] = None, + prompt: Optional[str] = None, + skip_circuits: Optional[Set[str]] = None, + max_concurrent: int = 4, + state_file: Optional[Path] = None +) -> Dict[str, any]: + """Analyze SKiDL circuits using parallel LLM analysis. + + Args: + source: Path to SKiDL source (file or directory) + output_file: Path to save analysis results + api_key: API key for cloud LLM services + backend: LLM backend to use + model: Specific model to use for analysis + prompt: Custom analysis prompt + skip_circuits: Set of circuit names to skip + max_concurrent: Maximum number of concurrent analyses + state_file: Path to save/load analysis state + + Returns: + Dictionary containing analysis results and metrics + + Raises: + RuntimeError: If analysis pipeline fails + """ + pipeline_start_time = time.time() + skip_circuits = skip_circuits or set() + + # Initialize or load state + state = (AnalysisState.load_state(state_file) + if state_file and state_file.exists() + else AnalysisState()) + + if skip_circuits: + logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") + + # Add source directory to Python path for imports + sys.path.insert(0, str(source.parent if source.is_file() else source)) + try: + # Import and execute circuit definition + module_name = source.stem if source.is_file() else 'main' + logger.info(f"Importing {module_name} module...") + module = importlib.import_module(module_name) + importlib.reload(module) + + if hasattr(module, 'main'): + logger.info("Executing circuit main()...") + module.main() + + # Collect circuits to analyze + hierarchies = set() + for part in default_circuit.parts: + if part.hierarchy != default_circuit.hierarchy: + if (part.hierarchy not in skip_circuits and + part.hierarchy not in state.completed): + hierarchies.add(part.hierarchy) + + if hierarchies: + logger.info(f"Starting parallel analysis of {len(hierarchies)} circuits...") + with ThreadPoolExecutor(max_workers=max_concurrent) as executor: + futures = [] + for circuit in sorted(hierarchies): + future = executor.submit( + analyze_single_circuit, + circuit=circuit, + api_key=api_key, + backend=backend, + model=model, + prompt=prompt, + state=state + ) + futures.append(future) + + for future in as_completed(futures): + try: + future.result() + except Exception as e: + logger.error(f"Thread failed: {str(e)}") + + if state_file: + state.save_state(state_file) + + # Generate consolidated report + consolidated_text = ["=== Circuit Analysis Report ===\n"] + + if state.completed: + consolidated_text.append("\n=== Successful Analyses ===") + for circuit in sorted(state.completed): + result = state.results[circuit] + consolidated_text.append(f"\n{'='*20} {circuit} {'='*20}\n") + consolidated_text.append(result.get("analysis", "No analysis available")) + token_info = ( + f"\nTokens used: {result.get('total_tokens', 0)} " + f"(Prompt: {result.get('prompt_tokens', 0)}, " + f"Completion: {result.get('completion_tokens', 0)})" + ) + consolidated_text.append(token_info) + + if state.failed: + consolidated_text.append("\n=== Failed Analyses ===") + for circuit, error in sorted(state.failed.items()): + consolidated_text.append(f"\n{circuit}: {error}") + + # Save consolidated results + if output_file: + with open(output_file, "w") as f: + f.write("\n".join(consolidated_text)) + + # Calculate total metrics + total_tokens = sum( + result.get("total_tokens", 0) + for result in state.results.values() + ) + + return { + "success": len(state.completed) > 0 and not state.failed, + "completed_circuits": sorted(state.completed), + "failed_circuits": state.failed, + "results": state.results, + "total_time_seconds": time.time() - pipeline_start_time, + "total_analysis_time": state.total_analysis_time, + "total_tokens": total_tokens + } + + finally: + sys.path.pop(0) \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/cli.py b/src/skidl/scripts/llm_analysis/cli.py new file mode 100644 index 000000000..4f4ca31cd --- /dev/null +++ b/src/skidl/scripts/llm_analysis/cli.py @@ -0,0 +1,231 @@ +"""Command-line interface for KiCad/SKiDL circuit analysis.""" + +import sys +import time +import argparse +from pathlib import Path +from typing import Set + +from skidl.pckg_info import __version__ + +from .config import Backend +from .logging import configure_logging, log_analysis_results, log_backend_help +from .kicad import get_default_kicad_cli, handle_kicad_libraries +from .generator import generate_netlist, generate_skidl_project, get_skidl_source +from .analyzer import analyze_circuits + +def parse_args() -> argparse.Namespace: + """Parse command line arguments. + + Returns: + Parsed command line arguments + """ + parser = argparse.ArgumentParser( + description="A tool for analyzing KiCad/SKiDL circuits using LLMs.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", "-v", + action="version", + version="skidl " + __version__ + ) + + # Input source group (mutually exclusive) + source_group = parser.add_mutually_exclusive_group(required=True) + source_group.add_argument( + "--schematic", "-s", + help="Path to KiCad schematic (.kicad_sch) file" + ) + source_group.add_argument( + "--netlist", "-n", + help="Path to netlist (.net) file" + ) + source_group.add_argument( + "--skidl", + help="Path to SKiDL Python file to analyze" + ) + source_group.add_argument( + "--skidl-dir", + help="Path to SKiDL project directory" + ) + + # Operation mode flags + parser.add_argument( + "--generate-netlist", + action="store_true", + help="Generate netlist from schematic" + ) + parser.add_argument( + "--generate-skidl", + action="store_true", + help="Generate SKiDL project from netlist" + ) + parser.add_argument( + "--analyze", + action="store_true", + help="Run LLM analysis on circuits" + ) + + # Optional configuration + parser.add_argument( + "--kicad-cli", + default=get_default_kicad_cli(), + help="Path to kicad-cli executable" + ) + parser.add_argument( + "--output-dir", "-o", + default=".", + help="Output directory for generated files" + ) + parser.add_argument( + "--api-key", + help="OpenRouter API key for cloud LLM analysis" + ) + parser.add_argument( + "--backend", + choices=["openrouter", "ollama"], + default="openrouter", + help="LLM backend to use" + ) + parser.add_argument( + "--model", + help="LLM model name for selected backend" + ) + parser.add_argument( + "--analysis-output", + default="circuit_analysis.txt", + help="Output file for analysis results" + ) + parser.add_argument( + "--analysis-prompt", + help="Custom prompt for circuit analysis" + ) + parser.add_argument( + "--skip-circuits", + help="Comma-separated list of circuits to skip during analysis" + ) + parser.add_argument( + "--max-concurrent", + type=int, + default=4, + help="Maximum number of concurrent LLM analyses (default: 4)" + ) + parser.add_argument( + "--kicad-lib-paths", + nargs="*", + help="List of custom KiCad library paths" + ) + parser.add_argument( + "--debug", "-d", + nargs="?", + type=int, + default=0, + metavar="LEVEL", + help="Print debugging info. (Larger LEVEL means more info.)" + ) + + return parser.parse_args() + +def validate_args(args: argparse.Namespace) -> None: + """Validate command line argument combinations. + + Args: + args: Parsed command line arguments + + Raises: + SystemExit: If invalid argument combinations are detected + """ + if args.generate_netlist and not args.schematic: + sys.exit("--generate-netlist requires --schematic") + if args.generate_skidl and not (args.netlist or args.generate_netlist): + sys.exit("--generate-skidl requires --netlist or --generate-netlist") + if args.analyze and args.backend == "openrouter" and not args.api_key: + sys.exit("OpenRouter backend requires --api-key") + +def main() -> None: + """Main entry point for the KiCad-SKiDL-LLM pipeline.""" + args = parse_args() + configure_logging(args.debug) + + try: + start_time = time.time() + print("Starting KiCad-SKiDL-LLM pipeline") + + # Validate arguments + validate_args(args) + + # Setup environment + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Add KiCad library paths if provided + if args.kicad_lib_paths: + handle_kicad_libraries(args.kicad_lib_paths) + + # Process input source and generate required files + netlist_path = None + skidl_dir = None + + if args.schematic or args.netlist: + if args.netlist: + netlist_path = Path(args.netlist) + if not netlist_path.exists(): + raise FileNotFoundError(f"Netlist not found: {netlist_path}") + print(f"Using existing netlist: {netlist_path}") + else: + netlist_path = generate_netlist( + Path(args.schematic), + output_dir, + args.kicad_cli + ) + + if args.generate_skidl: + skidl_dir = generate_skidl_project(netlist_path, output_dir) + + # Run circuit analysis if requested + if args.analyze: + skidl_source = get_skidl_source( + skidl_file=Path(args.skidl) if args.skidl else None, + skidl_dir=Path(args.skidl_dir) if args.skidl_dir else None, + generated_dir=skidl_dir + ) + + # Parse skip circuits if provided + skip_circuits: Set[str] = set() + if args.skip_circuits: + skip_circuits = {c.strip() for c in args.skip_circuits.split(",")} + + try: + results = analyze_circuits( + source=skidl_source, + output_file=Path(args.analysis_output), + api_key=args.api_key, + backend=Backend(args.backend), + model=args.model, + prompt=args.analysis_prompt, + skip_circuits=skip_circuits, + max_concurrent=args.max_concurrent + ) + + if results["success"]: + log_analysis_results(results) + print(f"\nAnalysis results saved to: {args.analysis_output}") + else: + raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") + + except Exception as e: + print("✗ Circuit analysis failed!") + print(f"Error: {str(e)}") + log_backend_help(args.backend) + sys.exit(1) + + total_time = time.time() - start_time + print(f"\nPipeline completed in {total_time:.2f} seconds") + + except Exception as e: + print(f"Pipeline failed: {str(e)}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/config.py b/src/skidl/scripts/llm_analysis/config.py new file mode 100644 index 000000000..21c99d114 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/config.py @@ -0,0 +1,25 @@ +"""Configuration settings and constants for KiCad/SKiDL LLM analysis.""" + +from enum import Enum + +# Default timeout for LLM response (seconds) +DEFAULT_TIMEOUT: int = 300 + +# Default LLM model +DEFAULT_MODEL: str = "google/gemini-2.0-flash-001" + +class Backend(Enum): + """Supported LLM backends.""" + OPENROUTER = "openrouter" + OLLAMA = "ollama" + + @classmethod + def from_str(cls, backend: str) -> 'Backend': + """Convert string to Backend enum, with validation.""" + try: + return cls(backend.lower()) + except ValueError: + valid_backends = ", ".join(b.value for b in cls) + raise ValueError( + f"Invalid backend: {backend}. Valid options: {valid_backends}" + ) \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/generator.py b/src/skidl/scripts/llm_analysis/generator.py new file mode 100644 index 000000000..5391e74eb --- /dev/null +++ b/src/skidl/scripts/llm_analysis/generator.py @@ -0,0 +1,115 @@ +"""Netlist and SKiDL project generation utilities.""" + +import subprocess +import logging +from pathlib import Path +from typing import Optional + +from .kicad import validate_kicad_cli + +logger = logging.getLogger("kicad_skidl_llm") + +def generate_netlist( + schematic_path: Path, + output_dir: Path, + kicad_cli_path: str +) -> Optional[Path]: + """Generate netlist from KiCad schematic. + + Args: + schematic_path: Path to KiCad schematic file + output_dir: Directory to save generated netlist + kicad_cli_path: Path to KiCad CLI executable + + Returns: + Path to generated netlist, or None if generation was skipped + + Raises: + FileNotFoundError: If schematic file doesn't exist + RuntimeError: If netlist generation fails + """ + if not schematic_path.exists(): + raise FileNotFoundError(f"Schematic not found: {schematic_path}") + + kicad_cli = validate_kicad_cli(kicad_cli_path) + netlist_path = output_dir / f"{schematic_path.stem}.net" + + try: + subprocess.run([ + kicad_cli, 'sch', 'export', 'netlist', + '-o', str(netlist_path), str(schematic_path) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e + + logger.info(f"✓ Generated netlist: {netlist_path}") + return netlist_path + +def generate_skidl_project( + netlist_path: Path, + output_dir: Path +) -> Optional[Path]: + """Generate SKiDL project from netlist. + + Args: + netlist_path: Path to netlist file + output_dir: Directory to save generated SKiDL project + + Returns: + Path to generated SKiDL project directory, or None if generation was skipped + + Raises: + RuntimeError: If SKiDL project generation fails + """ + skidl_dir = output_dir / f"{netlist_path.stem}_SKIDL" + skidl_dir.mkdir(parents=True, exist_ok=True) + + try: + subprocess.run([ + 'netlist_to_skidl', + '-i', str(netlist_path), + '--output', str(skidl_dir) + ], check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e + + logger.info(f"✓ Generated SKiDL project: {skidl_dir}") + return skidl_dir + +def get_skidl_source( + skidl_file: Optional[Path] = None, + skidl_dir: Optional[Path] = None, + generated_dir: Optional[Path] = None +) -> Path: + """Determine the SKiDL source to analyze. + + This function implements a priority order for determining which + SKiDL source to use for analysis: + 1. Explicitly provided SKiDL file + 2. Explicitly provided SKiDL directory + 3. Generated SKiDL project directory + + Args: + skidl_file: Path to specific SKiDL Python file + skidl_dir: Path to SKiDL project directory + generated_dir: Path to automatically generated SKiDL project + + Returns: + Path to SKiDL source to analyze + + Raises: + ValueError: If no valid SKiDL source is available + FileNotFoundError: If specified source doesn't exist + """ + if skidl_file: + if not skidl_file.exists(): + raise FileNotFoundError(f"SKiDL file not found: {skidl_file}") + return skidl_file + elif skidl_dir: + if not skidl_dir.exists(): + raise FileNotFoundError(f"SKiDL directory not found: {skidl_dir}") + return skidl_dir + elif generated_dir: + return generated_dir + else: + raise ValueError("No SKiDL source available for analysis") \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/kicad.py b/src/skidl/scripts/llm_analysis/kicad.py new file mode 100644 index 000000000..8f6de6ba9 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/kicad.py @@ -0,0 +1,129 @@ +"""KiCad integration utilities for circuit analysis.""" + +import os +import platform +from pathlib import Path +from typing import Optional, List +import logging + +from skidl import lib_search_paths, KICAD + +logger = logging.getLogger("kicad_skidl_llm") + +def validate_kicad_cli(path: str) -> str: + """Validate KiCad CLI executable and provide platform-specific guidance. + + Args: + path: Path to KiCad CLI executable + + Returns: + Validated path to KiCad CLI + + Raises: + FileNotFoundError: If KiCad CLI is not found + PermissionError: If KiCad CLI is not executable + """ + system = platform.system().lower() + cli_path = Path(path) + + if not cli_path.exists(): + suggestions = { + 'darwin': [ + "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", + "~/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + ], + 'windows': [ + r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe", + r"C:\Program Files (x86)\KiCad\7.0\bin\kicad-cli.exe" + ], + 'linux': [ + "/usr/bin/kicad-cli", + "/usr/local/bin/kicad-cli" + ] + } + + error_msg = [f"KiCad CLI not found: {path}"] + if system in suggestions: + error_msg.append("\nCommon paths for your platform:") + for suggestion in suggestions[system]: + error_msg.append(f" - {suggestion}") + error_msg.append("\nSpecify the correct path using --kicad-cli") + raise FileNotFoundError('\n'.join(error_msg)) + + if not os.access(str(cli_path), os.X_OK): + if system == 'windows': + raise PermissionError( + f"KiCad CLI not executable: {path}\n" + "Ensure the file exists and you have appropriate permissions." + ) + else: + raise PermissionError( + f"KiCad CLI not executable: {path}\n" + f"Try making it executable with: chmod +x {path}" + ) + + return str(cli_path) + +def get_default_kicad_cli() -> str: + """Get the default KiCad CLI path based on the current platform. + + Returns: + Platform-specific default path to KiCad CLI executable + """ + system = platform.system().lower() + if system == 'darwin': # macOS + return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" + elif system == 'windows': + return r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe" + else: # Linux and others + return "/usr/bin/kicad-cli" + +def handle_kicad_libraries(lib_paths: Optional[List[str]]) -> None: + """Add and validate KiCad library paths. + + Args: + lib_paths: List of paths to KiCad symbol libraries + + Raises: + RuntimeError: If no valid library paths are found + """ + valid_paths = [] + invalid_paths = [] + + # First, clear any existing paths to avoid duplicates + lib_search_paths[KICAD] = [] + + # Add system KiCad library path from environment variable + system_lib_path = os.environ.get('KICAD_SYMBOL_DIR') + if system_lib_path: + path = Path(system_lib_path) + if path.is_dir(): + lib_search_paths[KICAD].append(str(path)) + valid_paths.append(path) + else: + logger.warning(f"KICAD_SYMBOL_DIR path does not exist: {path}") + else: + logger.warning("KICAD_SYMBOL_DIR environment variable not set") + + # Process any additional user-provided paths + if lib_paths: + for lib_path in lib_paths: + path = Path(lib_path) + if not path.is_dir(): + invalid_paths.append((path, "Directory does not exist")) + continue + + sym_files = list(path.glob("*.kicad_sym")) + if not sym_files: + invalid_paths.append((path, "No .kicad_sym files found")) + continue + + valid_paths.append(path) + if str(path) not in lib_search_paths[KICAD]: + lib_search_paths[KICAD].append(str(path)) + + if not valid_paths: + raise RuntimeError( + "No valid KiCad library paths found. Please ensure KICAD_SYMBOL_DIR " + "is set correctly and/or provide valid library paths." + ) \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/logging.py b/src/skidl/scripts/llm_analysis/logging.py new file mode 100644 index 000000000..9736e5f75 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/logging.py @@ -0,0 +1,70 @@ +"""Logging configuration for circuit analysis.""" + +import sys +import logging +from typing import Optional + +logger = logging.getLogger("kicad_skidl_llm") + +def configure_logging(debug_level: Optional[int] = None) -> None: + """Configure logging for the circuit analysis pipeline. + + Sets up a StreamHandler with appropriate formatting and level. + Debug levels work inversely - higher number means more verbose output. + + Args: + debug_level: Debug level (None for no debug, or 0+ for increasing verbosity) + """ + if debug_level is not None: + # Calculate log level - higher debug_level means lower logging level + log_level = logging.DEBUG + 1 - debug_level + + # Create and configure handler + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter( + "[%(asctime)s] %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + )) + handler.setLevel(log_level) + + # Configure logger + logger.addHandler(handler) + logger.setLevel(log_level) + else: + # If no debug level specified, only show INFO and above + logger.setLevel(logging.INFO) + +def log_analysis_results(results: dict) -> None: + """Log analysis results summary. + + Args: + results: Dictionary containing analysis results and metrics + """ + if results["success"]: + logger.info("\nAnalysis Results:") + logger.info(f" ✓ Completed Circuits: {len(results['completed_circuits'])}") + logger.info(f" ✓ Total Analysis Time: {results['total_analysis_time']:.2f} seconds") + logger.info(f" ✓ Total Pipeline Time: {results['total_time_seconds']:.2f} seconds") + if results.get("total_tokens"): + logger.info(f" ✓ Total Tokens Used: {results['total_tokens']}") + + if results["failed_circuits"]: + logger.warning("\nFailed Circuits:") + for circuit, error in results["failed_circuits"].items(): + logger.warning(f" ✗ {circuit}: {error}") + +def log_backend_help(backend: str) -> None: + """Log backend-specific troubleshooting tips. + + Args: + backend: Name of the LLM backend + """ + if backend == "openrouter": + logger.error("\nTroubleshooting tips:") + logger.error("1. Check your API key") + logger.error("2. Verify you have sufficient API credits") + logger.error("3. Check for rate limiting") + else: # ollama + logger.error("\nTroubleshooting tips:") + logger.error("1. Verify Ollama is running locally") + logger.error("2. Check if the requested model is installed") \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/prompts/__init__.py b/src/skidl/scripts/llm_analysis/prompts/__init__.py new file mode 100644 index 000000000..4c9eed0a4 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/prompts/__init__.py @@ -0,0 +1,14 @@ +"""Circuit analysis prompt templates. + +This package provides templates for generating circuit analysis prompts. +The prompts are organized into: +1. Base prompt structure and methodology +2. Individual analysis section templates +""" + +from .base import get_base_prompt +from .sections import ANALYSIS_SECTIONS + +__version__ = "1.0.0" + +__all__ = ["get_base_prompt", "ANALYSIS_SECTIONS"] \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/prompts/base.py b/src/skidl/scripts/llm_analysis/prompts/base.py new file mode 100644 index 000000000..dc4343418 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/prompts/base.py @@ -0,0 +1,109 @@ +"""Base prompt template for circuit analysis.""" + +__version__ = "1.0.0" + +BASE_METHODOLOGY = """ +ANALYSIS METHODOLOGY: +1. Begin analysis immediately with available information +2. After completing analysis, identify any critical missing information needed for deeper insights +3. Begin with subcircuit identification and individual analysis +4. Analyze interactions between subcircuits +5. Evaluate system-level performance and integration +6. Assess manufacturing and practical implementation considerations +""" + +SECTION_REQUIREMENTS = """ +For each analysis section: +1. Analyze with available information first +2. Start with critical missing information identification +3. Provide detailed technical analysis with calculations +4. Include specific numerical criteria and measurements +5. Reference relevant industry standards +6. Provide concrete recommendations +7. Prioritize findings by severity +8. Include specific action items +""" + +ISSUE_FORMAT = """ +For each identified issue: +SEVERITY: (Critical/High/Medium/Low) +CATEGORY: (Design/Performance/Safety/Manufacturing/etc.) +SUBCIRCUIT: Affected subcircuit or system level +DESCRIPTION: Detailed issue description +IMPACT: Quantified impact on system performance +VERIFICATION: How to verify the issue exists +RECOMMENDATION: Specific action items with justification +STANDARDS: Applicable industry standards +TRADE-OFFS: Impact of proposed changes +PRIORITY: Implementation priority level +""" + +SPECIAL_REQUIREMENTS = """ +Special Requirements: +- Analyze each subcircuit completely before moving to system-level analysis +- Provide specific component recommendations where applicable +- Include calculations and formulas used in analysis +- Reference specific standards and requirements +- Consider worst-case scenarios +- Evaluate corner cases +- Assess impact of component variations +- Consider environmental effects +- Evaluate aging effects +- Assess maintenance requirements +""" + +OUTPUT_FORMAT = """ +Output Format: +1. Executive Summary +2. Critical Findings Summary +3. Detailed Subcircuit Analysis (one section per subcircuit) +4. System-Level Analysis +5. Cross-Cutting Concerns +6. Recommendations Summary +7. Required Action Items (prioritized) +8. Additional Information Needed +""" + +IMPORTANT_INSTRUCTIONS = """ +IMPORTANT INSTRUCTIONS: +- Start analysis immediately - do not acknowledge the request or state that you will analyze +- Be specific and quantitative where possible +- Include calculations and methodology +- Reference specific standards +- Provide actionable recommendations +- Consider practical implementation +- Evaluate cost implications +- Assess manufacturing feasibility +- Consider maintenance requirements +""" + +def get_base_prompt(circuit_description: str, analysis_sections: str) -> str: + """ + Generate the complete base analysis prompt. + + Args: + circuit_description: Description of the circuit to analyze + analysis_sections: String containing enabled analysis sections + + Returns: + Complete formatted base prompt + """ + return f""" +You are an expert electronics engineer. Analyze the following circuit design immediately and provide actionable insights. Do not acknowledge the request or promise to analyze - proceed directly with your analysis. + +Circuit Description: +{circuit_description} + +{BASE_METHODOLOGY} + +REQUIRED ANALYSIS SECTIONS: +{analysis_sections} + +{SECTION_REQUIREMENTS} +{ISSUE_FORMAT} +{SPECIAL_REQUIREMENTS} +{OUTPUT_FORMAT} +{IMPORTANT_INSTRUCTIONS} + +After completing your analysis, if additional information would enable deeper insights, list specific questions in a separate section titled 'Additional Information Needed' at the end. +""".strip() \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/prompts/sections.py b/src/skidl/scripts/llm_analysis/prompts/sections.py new file mode 100644 index 000000000..9699e7e03 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/prompts/sections.py @@ -0,0 +1,133 @@ +"""Analysis section prompt templates.""" + +from typing import Dict + +__version__ = "1.0.0" + +SYSTEM_OVERVIEW: str = """ +0. System-Level Analysis: +- Comprehensive system architecture review +- Interface analysis between major blocks +- System-level timing and synchronization +- Resource allocation and optimization +- System-level failure modes +- Integration challenges +- Performance bottlenecks +- Scalability assessment +""".strip() + +DESIGN_REVIEW: str = """ +1. Comprehensive Design Architecture Review: +- Evaluate overall hierarchical structure +- Assess modularity and reusability +- Interface protocols analysis +- Control path verification +- Design pattern evaluation +- Critical path analysis +- Feedback loop stability +- Clock domain analysis +- Reset strategy review +- State machine verification +- Resource utilization assessment +- Design rule compliance +""".strip() + +POWER_ANALYSIS: str = """ +2. In-depth Power Distribution Analysis: +- Complete power tree mapping +- Voltage drop calculations +- Current distribution analysis +- Power sequencing requirements +- Brownout behavior analysis +- Load transient response +- Power supply rejection ratio +- Efficiency optimization +- Thermal implications +- Battery life calculations (if applicable) +- Power integrity simulation +- Decoupling strategy +- Ground bounce analysis +""".strip() + +SIGNAL_INTEGRITY: str = """ +3. Detailed Signal Integrity Analysis: +- Critical path timing analysis +- Setup/hold time verification +- Clock skew analysis +- Propagation delay calculations +- Cross-talk assessment +- Reflection analysis +- EMI/EMC considerations +- Signal loading effects +- Impedance matching +- Common mode noise rejection +- Ground loop analysis +- Shield effectiveness +""".strip() + +THERMAL_ANALYSIS: str = """ +4. Thermal Performance Analysis: +- Component temperature rise calculations +- Thermal resistance analysis +- Heat spreading patterns +- Cooling requirements +- Thermal gradient mapping +- Hot spot identification +- Thermal cycling effects +- Temperature derating +- Thermal protection mechanisms +- Cooling solution optimization +""".strip() + +NOISE_ANALYSIS: str = """ +5. Comprehensive Noise Analysis: +- Noise source identification +- Noise coupling paths +- Ground noise analysis +- Power supply noise +- Digital switching noise +- RF interference +- Common mode noise +- Differential mode noise +- Shielding effectiveness +- Filter performance +- Noise margin calculations +""".strip() + +TESTING_VERIFICATION: str = """ +6. Testing and Verification Strategy: +- Functional test coverage +- Performance verification +- Environmental testing +- Reliability testing +- Safety verification +- EMC/EMI testing +- Production test strategy +- Self-test capabilities +- Calibration requirements +- Diagnostic capabilities +- Test point access +- Debug interface requirements +""".strip() + +CAP_DC_BIAS_DERATING: str = """ +7. Capacitor DC Bias Derating Analysis: +- Identification of capacitors susceptible to DC bias derating +- Calculation of effective capacitance at operating voltage +- Impact on circuit performance (e.g., filter cutoff frequency, timing) +- Recommendation of alternative capacitor types or values +- Consideration of temperature effects on DC bias derating +- Verification of capacitance derating with manufacturer data +""".strip() + +# Dictionary mapping section names to their prompts +ANALYSIS_SECTIONS: Dict[str, str] = { + "system_overview": SYSTEM_OVERVIEW, + "design_review": DESIGN_REVIEW, + "power_analysis": POWER_ANALYSIS, + "signal_integrity": SIGNAL_INTEGRITY, + "thermal_analysis": THERMAL_ANALYSIS, + "noise_analysis": NOISE_ANALYSIS, + "testing_verification": TESTING_VERIFICATION, + "cap_dc_bias_derating": CAP_DC_BIAS_DERATING +} \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/state.py b/src/skidl/scripts/llm_analysis/state.py new file mode 100644 index 000000000..88e56ca71 --- /dev/null +++ b/src/skidl/scripts/llm_analysis/state.py @@ -0,0 +1,115 @@ +"""State management for circuit analysis tracking across threads.""" + +import json +import threading +from dataclasses import dataclass, field +from pathlib import Path +from typing import Set, Dict, Optional + +@dataclass +class AnalysisState: + """Tracks the state of circuit analysis across threads. + + This class provides thread-safe tracking of completed circuits, + failed analyses, and results. It also supports saving and loading + state from disk for resumable analysis sessions. + + Attributes: + completed: Set of completed circuit names + failed: Dictionary mapping failed circuit names to error messages + results: Dictionary mapping circuit names to analysis results + lock: Thread lock for synchronization + total_analysis_time: Total time spent in analysis + """ + completed: Set[str] = field(default_factory=set) + failed: Dict[str, str] = field(default_factory=dict) + results: Dict[str, dict] = field(default_factory=dict) + lock: threading.Lock = field(default_factory=threading.Lock) + total_analysis_time: float = 0.0 + + def save_state(self, path: Path) -> None: + """Save current analysis state to disk. + + Args: + path: Path to save state file + """ + with self.lock: + state_dict = { + "completed": list(self.completed), + "failed": self.failed, + "results": self.results, + "total_analysis_time": self.total_analysis_time + } + with open(path, 'w') as f: + json.dump(state_dict, f, indent=2) + + @classmethod + def load_state(cls, path: Path) -> 'AnalysisState': + """Load analysis state from disk. + + Args: + path: Path to state file + + Returns: + Loaded AnalysisState instance + """ + with open(path) as f: + state_dict = json.load(f) + state = cls() + state.completed = set(state_dict["completed"]) + state.failed = state_dict["failed"] + state.results = state_dict["results"] + state.total_analysis_time = state_dict.get("total_analysis_time", 0.0) + return state + + def add_result(self, circuit: str, result: dict) -> None: + """Thread-safe addition of analysis result. + + Args: + circuit: Name of the analyzed circuit + result: Analysis result dictionary + """ + with self.lock: + self.results[circuit] = result + self.completed.add(circuit) + if "request_time_seconds" in result: + self.total_analysis_time += result["request_time_seconds"] + + def add_failure(self, circuit: str, error: str) -> None: + """Thread-safe recording of analysis failure. + + Args: + circuit: Name of the failed circuit + error: Error message describing the failure + """ + with self.lock: + self.failed[circuit] = error + + def get_circuit_status(self, circuit: str) -> Optional[str]: + """Get the status of a specific circuit. + + Args: + circuit: Name of the circuit to check + + Returns: + 'completed', 'failed', or None if not processed + """ + with self.lock: + if circuit in self.completed: + return 'completed' + if circuit in self.failed: + return 'failed' + return None + + def get_summary(self) -> Dict[str, int]: + """Get summary statistics of analysis state. + + Returns: + Dictionary with counts of completed, failed, and total circuits + """ + with self.lock: + return { + "completed": len(self.completed), + "failed": len(self.failed), + "total": len(self.completed) + len(self.failed) + } \ No newline at end of file From 4110075c0300c9e7b20eb55f93ee20f16dd49c83 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 15:49:00 -0800 Subject: [PATCH 70/73] add readme, remove old file --- kicad_skidl_llm.py | 676 ----------------------- src/skidl/scripts/llm_analysis/README.md | 192 +++++++ 2 files changed, 192 insertions(+), 676 deletions(-) delete mode 100644 kicad_skidl_llm.py create mode 100644 src/skidl/scripts/llm_analysis/README.md diff --git a/kicad_skidl_llm.py b/kicad_skidl_llm.py deleted file mode 100644 index 534512cd0..000000000 --- a/kicad_skidl_llm.py +++ /dev/null @@ -1,676 +0,0 @@ -#!/usr/bin/env python3 -""" -KiCad-SKiDL-LLM Pipeline ------------------------- - -A tool for analyzing KiCad circuit designs using SKiDL and LLMs. -Supports direct SKiDL analysis, KiCad schematic conversion, and netlist processing. - -Features: - - KiCad schematic (.kicad_sch) to netlist conversion - - Netlist to SKiDL project generation - - Circuit analysis using OpenRouter or Ollama LLM backends - - Support for subcircuit analysis with selective circuit skipping - - Detailed logging and timing information - - Parallel analysis of subcircuits - -Example Usage: - # Analyze KiCad schematic - python kicad_skidl_llm.py --schematic design.kicad_sch --generate-netlist \ - --generate-skidl --analyze --api-key YOUR_KEY - - # Analyze existing SKiDL project using Ollama - python kicad_skidl_llm.py --skidl-dir project/ --analyze --backend ollama - - # Skip specific circuits during analysis - python kicad_skidl_llm.py --skidl circuit.py --analyze \ - --skip-circuits "voltage_regulator1,adc_interface2" -""" - -import os -import sys -import platform -import subprocess -import importlib -import textwrap -from pathlib import Path -from typing import List, Dict, Optional, Union, Set -import argparse -import logging -from datetime import datetime -import time -from enum import Enum -import threading -from concurrent.futures import ThreadPoolExecutor, as_completed -from dataclasses import dataclass, field -import json - -from skidl import * - -# Global configuration -DEFAULT_TIMEOUT = 300 # Maximum time to wait for LLM response -DEFAULT_MODEL = "google/gemini-2.0-flash-001" # Default LLM model -DEFAULT_OUTPUT_DIR = Path.cwd() / "output" # Default output directory - -class Backend(Enum): - """Supported LLM backends.""" - OPENROUTER = "openrouter" - OLLAMA = "ollama" - -# Configure logging -logger = logging.getLogger('kicad_skidl_llm') -logger.setLevel(logging.INFO) -formatter = logging.Formatter('[%(asctime)s] %(levelname)s: %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') -handler = logging.StreamHandler() -handler.setFormatter(formatter) -logger.addHandler(handler) - -# Disable propagation of SKiDL loggers to prevent duplicate messages -skidl_loggers = ['skidl.logger', 'skidl.active_logger', 'skidl.erc_logger'] -for name in skidl_loggers: - l = logging.getLogger(name) - l.propagate = False - -class CircuitDiscoveryError(Exception): - """Raised when there are issues discovering or loading circuits.""" - pass - -@dataclass -class AnalysisState: - """ - Tracks the state of circuit analysis across threads. - - This class maintains thread-safe state information about ongoing circuit analyses, - including completed circuits, failures, and timing metrics. - """ - completed: Set[str] = field(default_factory=set) - failed: Dict[str, str] = field(default_factory=dict) # circuit -> error message - results: Dict[str, Dict] = field(default_factory=dict) - lock: threading.Lock = field(default_factory=threading.Lock) - total_analysis_time: float = 0.0 - - def save_state(self, path: Path): - """Save current analysis state to disk.""" - with self.lock: - state_dict = { - "completed": list(self.completed), - "failed": self.failed, - "results": self.results, - "total_analysis_time": self.total_analysis_time - } - with open(path, 'w') as f: - json.dump(state_dict, f, indent=2) - - @classmethod - def load_state(cls, path: Path) -> 'AnalysisState': - """Load analysis state from disk.""" - with open(path) as f: - state_dict = json.load(f) - state = cls() - state.completed = set(state_dict["completed"]) - state.failed = state_dict["failed"] - state.results = state_dict["results"] - state.total_analysis_time = state_dict.get("total_analysis_time", 0.0) - return state - - def add_result(self, circuit: str, result: Dict): - """Thread-safe addition of analysis result.""" - with self.lock: - self.results[circuit] = result - self.completed.add(circuit) - # Add the analysis time from this result - if "request_time_seconds" in result: - self.total_analysis_time += result["request_time_seconds"] - - def add_failure(self, circuit: str, error: str): - """Thread-safe recording of analysis failure.""" - with self.lock: - self.failed[circuit] = error - -def analyze_single_circuit( - circuit: str, - api_key: Optional[str], - backend: Backend, - model: Optional[str], - prompt: Optional[str], - state: AnalysisState -) -> None: - """ - Analyze a single circuit and update the shared state. - - Args: - circuit: Hierarchy name of circuit to analyze - api_key: API key for LLM service - backend: LLM backend to use - model: Model name for selected backend - prompt: Custom analysis prompt - state: Shared analysis state object - """ - try: - start_time = time.time() - - # Get description focused on this circuit - circuit_desc = default_circuit.get_circuit_info(hierarchy=circuit, depth=1) - - # Analyze just this circuit - result = default_circuit.analyze_with_llm( - api_key=api_key, - output_file=None, - backend=backend.value, - model=model or DEFAULT_MODEL, - custom_prompt=prompt, - analyze_subcircuits=False - ) - - # Add timing information - result["request_time_seconds"] = time.time() - start_time - - state.add_result(circuit, result) - logger.info(f"✓ Completed analysis of {circuit}") - - except Exception as e: - error_msg = f"Analysis failed: {str(e)}" - state.add_failure(circuit, error_msg) - logger.error(f"✗ Failed analysis of {circuit}: {str(e)}") - -def analyze_circuits( - source: Path, - output_file: Path, - api_key: Optional[str] = None, - backend: Backend = Backend.OPENROUTER, - model: Optional[str] = None, - prompt: Optional[str] = None, - skip_circuits: Optional[Set[str]] = None, - max_concurrent: int = 4, - state_file: Optional[Path] = None -) -> Dict[str, Union[bool, float, int, Dict]]: - """ - Analyze SKiDL circuits using parallel LLM analysis. - - Args: - source: Path to SKiDL file or directory - output_file: Output file for analysis results - api_key: API key for LLM service - backend: LLM backend to use - model: Model name for selected backend - prompt: Custom analysis prompt - skip_circuits: Set of circuit hierarchies to skip - max_concurrent: Maximum number of concurrent analyses - state_file: Optional path to save/load analysis state - - Returns: - Dictionary containing analysis results and metadata - """ - pipeline_start_time = time.time() - skip_circuits = skip_circuits or set() - - # Load previous state if it exists - state = AnalysisState.load_state(state_file) if state_file and state_file.exists() else AnalysisState() - - if skip_circuits: - logger.info(f"Skipping circuits: {', '.join(sorted(skip_circuits))}") - - sys.path.insert(0, str(source.parent if source.is_file() else source)) - try: - # Import and setup phase - module_name = source.stem if source.is_file() else 'main' - logger.info(f"Importing {module_name} module...") - module = importlib.import_module(module_name) - importlib.reload(module) - - if hasattr(module, 'main'): - logger.info("Executing circuit main()...") - module.main() - - # Get circuits to analyze (excluding already completed ones) - hierarchies = set() - for part in default_circuit.parts: - if part.hierarchy != default_circuit.hierarchy: # Skip top level - if part.hierarchy not in skip_circuits and part.hierarchy not in state.completed: - hierarchies.add(part.hierarchy) - - if not hierarchies: - logger.info("No new circuits to analyze") - if state.completed: - logger.info(f"Previously analyzed: {len(state.completed)} circuits") - - else: - # Parallel analysis phase - logger.info(f"Starting parallel analysis of {len(hierarchies)} circuits...") - with ThreadPoolExecutor(max_workers=max_concurrent) as executor: - futures = [] - for circuit in sorted(hierarchies): - future = executor.submit( - analyze_single_circuit, - circuit=circuit, - api_key=api_key, - backend=backend, - model=model, - prompt=prompt, - state=state - ) - futures.append(future) - - # Wait for all analyses to complete - for future in as_completed(futures): - try: - future.result() # This will raise any exceptions from the thread - except Exception as e: - logger.error(f"Thread failed: {str(e)}") - - # Save intermediate state after each completion - if state_file: - state.save_state(state_file) - - # Generate consolidated report - consolidated_text = ["=== Circuit Analysis Report ===\n"] - - if state.completed: - consolidated_text.append("\n=== Successful Analyses ===") - for circuit in sorted(state.completed): - result = state.results[circuit] - consolidated_text.append(f"\n{'='*20} {circuit} {'='*20}") - consolidated_text.append(result.get("analysis", "No analysis available")) - token_info = ( - f"\nTokens used: {result.get('total_tokens', 0)} " - f"(Prompt: {result.get('prompt_tokens', 0)}, " - f"Completion: {result.get('completion_tokens', 0)})" - ) - consolidated_text.append(token_info) - - if state.failed: - consolidated_text.append("\n=== Failed Analyses ===") - for circuit, error in sorted(state.failed.items()): - consolidated_text.append(f"\n{circuit}: {error}") - - # Save consolidated results - if output_file: - with open(output_file, "w") as f: - f.write("\n".join(consolidated_text)) - - # Calculate total metrics - total_tokens = sum( - result.get("total_tokens", 0) - for result in state.results.values() - ) - - return { - "success": len(state.completed) > 0 and not state.failed, - "completed_circuits": sorted(state.completed), - "failed_circuits": state.failed, - "results": state.results, - "total_time_seconds": time.time() - pipeline_start_time, # Overall pipeline time - "total_analysis_time": state.total_analysis_time, # Cumulative analysis time - "total_tokens": total_tokens - } - - finally: - sys.path.pop(0) - -def validate_kicad_cli(path: str) -> str: - """ - Validate KiCad CLI executable and provide platform-specific guidance. - - Args: - path: Path to kicad-cli executable - - Returns: - Validated path - - Raises: - FileNotFoundError: If executable not found - PermissionError: If executable lacks permissions - """ - system = platform.system().lower() - cli_path = Path(path) - - if not cli_path.exists(): - suggestions = { - 'darwin': [ - "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli", - "~/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" - ], - 'windows': [ - r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe", - r"C:\Program Files (x86)\KiCad\7.0\bin\kicad-cli.exe" - ], - 'linux': [ - "/usr/bin/kicad-cli", - "/usr/local/bin/kicad-cli" - ] - } - - error_msg = [f"KiCad CLI not found: {path}"] - if system in suggestions: - error_msg.append("\nCommon paths for your platform:") - for suggestion in suggestions[system]: - error_msg.append(f" - {suggestion}") - error_msg.append("\nSpecify the correct path using --kicad-cli") - raise FileNotFoundError('\n'.join(error_msg)) - - if not os.access(str(cli_path), os.X_OK): - if platform.system().lower() == 'windows': - raise PermissionError( - f"KiCad CLI not executable: {path}\n" - "Ensure the file exists and you have appropriate permissions." - ) - else: - raise PermissionError( - f"KiCad CLI not executable: {path}\n" - "Try making it executable with: chmod +x {path}" - ) - - return str(cli_path) - -def get_default_kicad_cli() -> str: - """Get the default KiCad CLI path based on the current platform.""" - system = platform.system().lower() - if system == 'darwin': # macOS - return "/Applications/KiCad/KiCad.app/Contents/MacOS/kicad-cli" - elif system == 'windows': - return r"C:\Program Files\KiCad\7.0\bin\kicad-cli.exe" - else: # Linux and others - return "/usr/bin/kicad-cli" - -def parse_args() -> argparse.Namespace: - """Parse and validate command line arguments.""" - parser = argparse.ArgumentParser( - description='KiCad to SKiDL conversion and circuit analysis pipeline', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=textwrap.dedent(""" - Examples: - # Generate netlist and SKiDL project from schematic - %(prog)s --schematic design.kicad_sch --generate-netlist --generate-skidl --analyze --api-key YOUR_KEY - - # Analyze existing SKiDL project with circuit skipping - %(prog)s --skidl-dir project/ --analyze -# Skip specific circuits during analysis - %(prog)s --skidl-dir project/ --analyze --skip-circuits "circuit1,circuit2" --api-key YOUR_KEY - """) - ) - - # Input source group (mutually exclusive) - source_group = parser.add_mutually_exclusive_group(required=True) - source_group.add_argument('--schematic', '-s', help='Path to KiCad schematic (.kicad_sch) file') - source_group.add_argument('--netlist', '-n', help='Path to netlist (.net) file') - source_group.add_argument('--skidl', help='Path to SKiDL Python file to analyze') - source_group.add_argument('--skidl-dir', help='Path to SKiDL project directory') - - # Operation mode flags - parser.add_argument('--generate-netlist', action='store_true', help='Generate netlist from schematic') - parser.add_argument('--generate-skidl', action='store_true', help='Generate SKiDL project from netlist') - parser.add_argument('--analyze', action='store_true', help='Run LLM analysis on circuits') - - # Optional configuration - parser.add_argument('--kicad-cli', default=get_default_kicad_cli(), - help='Path to kicad-cli executable') - parser.add_argument('--output-dir', '-o', default='.', help='Output directory for generated files') - parser.add_argument('--api-key', help='OpenRouter API key for cloud LLM analysis') - parser.add_argument('--backend', choices=['openrouter', 'ollama'], default='openrouter', - help='LLM backend to use') - parser.add_argument('--model', help='LLM model name for selected backend') - parser.add_argument('--analysis-output', default='circuit_analysis.txt', - help='Output file for analysis results') - parser.add_argument('--analysis-prompt', help='Custom prompt for circuit analysis') - parser.add_argument('--skip-circuits', help='Comma-separated list of circuits to skip during analysis') - parser.add_argument('--kicad-lib-paths', nargs='*', - help='List of custom KiCad library paths') - parser.add_argument('--max-concurrent', type=int, default=4, - help='Maximum number of concurrent LLM analyses (default: 4)') - - args = parser.parse_args() - - # Validate argument combinations - if args.generate_netlist and not args.schematic: - parser.error("--generate-netlist requires --schematic") - if args.generate_skidl and not (args.netlist or args.generate_netlist): - parser.error("--generate-skidl requires --netlist or --generate-netlist") - if args.analyze and args.backend == 'openrouter' and not args.api_key: - parser.error("OpenRouter backend requires --api-key") - - return args - -def setup_environment(args) -> Path: - """Setup output directory and return its path.""" - output_dir = Path(args.output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - return output_dir - -def handle_kicad_libraries(lib_paths: Optional[List[str]]) -> None: - """Add and validate KiCad library paths. - - Args: - lib_paths: List of paths to KiCad library directories - """ - from skidl import lib_search_paths, KICAD - valid_paths = [] - invalid_paths = [] - - # First, clear any existing paths to avoid duplicates - lib_search_paths[KICAD] = [] - - # Add system KiCad library path from environment variable - system_lib_path = os.environ.get('KICAD_SYMBOL_DIR') - if system_lib_path: - path = Path(system_lib_path) - if path.is_dir(): - lib_search_paths[KICAD].append(str(path)) - valid_paths.append(path) - else: - logger.warning(f"KICAD_SYMBOL_DIR path does not exist: {path}") - else: - logger.warning("KICAD_SYMBOL_DIR environment variable not set") - logger.warning("Please set KICAD_SYMBOL_DIR to your KiCad symbols directory") - - # Then process any additional user-provided paths - if lib_paths: - for lib_path in lib_paths: - path = Path(lib_path) - if not path.is_dir(): - invalid_paths.append((path, "Directory does not exist")) - continue - - # Look for .kicad_sym files only - sym_files = list(path.glob("*.kicad_sym")) - if not sym_files: - invalid_paths.append((path, "No .kicad_sym files found")) - continue - - valid_paths.append(path) - # Add the path to SKiDL's library search paths - if str(path) not in lib_search_paths[KICAD]: - lib_search_paths[KICAD].append(str(path)) - - if valid_paths: - logger.info("Added KiCad library paths:") - for path in valid_paths: - logger.info(f" ✓ {path}") - sym_files = list(path.glob("*.kicad_sym")) - # Show up to 3 symbol files as examples - for sym in sym_files[:3]: - logger.info(f" - {sym.name}") - if len(sym_files) > 3: - logger.info(f" - ... and {len(sym_files)-3} more") - - if invalid_paths: - logger.warning("Invalid library paths:") - for path, reason in invalid_paths: - logger.warning(f" ✗ {path}: {reason}") - - if not valid_paths: - raise RuntimeError( - "No valid KiCad library paths found. Please ensure KICAD_SYMBOL_DIR " - "is set correctly and/or provide valid library paths." - ) - - # Debug output to verify paths are properly set - logger.debug("Final SKiDL library search paths:") - for path in lib_search_paths[KICAD]: - logger.debug(f" - {path}") - -def generate_netlist(args, output_dir: Path) -> Optional[Path]: - """Generate netlist from schematic if requested.""" - if not (args.schematic and args.generate_netlist): - return None - - logger.info("Step 1: Generating netlist from schematic...") - schematic_path = Path(args.schematic) - if not schematic_path.exists(): - raise FileNotFoundError(f"Schematic not found: {schematic_path}") - - kicad_cli = validate_kicad_cli(args.kicad_cli) - netlist_path = output_dir / f"{schematic_path.stem}.net" - - try: - subprocess.run([ - kicad_cli, 'sch', 'export', 'netlist', - '-o', str(netlist_path), str(schematic_path) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Netlist generation failed:\n{e.stderr}") from e - - logger.info(f"✓ Generated netlist: {netlist_path}") - return netlist_path - -def generate_skidl_project(args, current_netlist: Optional[Path], output_dir: Path) -> Optional[Path]: - """Generate SKiDL project from netlist if requested.""" - if not args.generate_skidl or not current_netlist: - return None - - logger.info("Step 2: Generating SKiDL project from netlist...") - skidl_dir = output_dir / f"{current_netlist.stem}_SKIDL" - skidl_dir.mkdir(parents=True, exist_ok=True) - - try: - subprocess.run([ - 'netlist_to_skidl', - '-i', str(current_netlist), - '--output', str(skidl_dir) - ], check=True, capture_output=True, text=True) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"SKiDL project generation failed:\n{e.stderr}") from e - - logger.info(f"✓ Generated SKiDL project: {skidl_dir}") - return skidl_dir - -def get_skidl_source(args, netlist_path: Optional[Path], skidl_dir: Optional[Path]) -> Path: - """Determine the SKiDL source to analyze.""" - if args.skidl: - source = Path(args.skidl) - if not source.exists(): - raise FileNotFoundError(f"SKiDL file not found: {source}") - logger.info(f"Using SKiDL file: {source}") - return source - elif args.skidl_dir: - source = Path(args.skidl_dir) - if not source.exists(): - raise FileNotFoundError(f"SKiDL directory not found: {source}") - logger.info(f"Using SKiDL project directory: {source}") - return source - elif skidl_dir: - return skidl_dir - else: - raise ValueError("No SKiDL source available for analysis") - -def main() -> None: - """ - Main execution function implementing the KiCad-SKiDL-LLM pipeline. - - The pipeline supports: - 1. KiCad schematic to netlist conversion - 2. Netlist to SKiDL project generation - 3. Circuit analysis using LLM - """ - start_time = time.time() - try: - logger.info("Starting KiCad-SKiDL-LLM pipeline") - args = parse_args() - - # Setup environment - output_dir = setup_environment(args) - - # Add KiCad library paths if provided - if args.kicad_lib_paths: - handle_kicad_libraries(args.kicad_lib_paths) - - # Process input source and generate required files - netlist_path = None - skidl_dir = None - - if args.schematic or args.netlist: - # Handle netlist - if args.netlist: - netlist_path = Path(args.netlist) - if not netlist_path.exists(): - raise FileNotFoundError(f"Netlist not found: {netlist_path}") - logger.info(f"Using existing netlist: {netlist_path}") - else: - netlist_path = generate_netlist(args, output_dir) - - # Generate SKiDL project if requested - if args.generate_skidl: - skidl_dir = generate_skidl_project(args, netlist_path, output_dir) - - # Run circuit analysis if requested - if args.analyze: - skidl_source = get_skidl_source(args, netlist_path, skidl_dir) - - # Parse skip circuits if provided - skip_circuits = set() - if args.skip_circuits: - skip_circuits = {c.strip() for c in args.skip_circuits.split(',')} - - try: - results = analyze_circuits( - source=skidl_source, - output_file=args.analysis_output, - api_key=args.api_key, - backend=Backend(args.backend), - model=args.model, - prompt=args.analysis_prompt, - skip_circuits=skip_circuits, - max_concurrent=args.max_concurrent - ) - - if results["success"]: - logger.info("\nAnalysis Results:") - logger.info(f" ✓ Completed Circuits: {len(results['completed_circuits'])}") - logger.info(f" ✓ Total Analysis Time: {results['total_analysis_time']:.2f} seconds") - logger.info(f" ✓ Total Pipeline Time: {results['total_time_seconds']:.2f} seconds") - if results.get('total_tokens'): - logger.info(f" ✓ Total Tokens Used: {results['total_tokens']}") - - if results["failed_circuits"]: - logger.warning("\nFailed Circuits:") - for circuit, error in results["failed_circuits"].items(): - logger.warning(f" ✗ {circuit}: {error}") - - logger.info(f"\nAnalysis results saved to: {args.analysis_output}") - else: - raise RuntimeError(f"Analysis failed: {results.get('error', 'Unknown error')}") - - except Exception as e: - logger.error("✗ Circuit analysis failed!") - logger.error(f"Error: {str(e)}") - if args.backend == 'openrouter': - logger.error("Troubleshooting tips:") - logger.error("1. Check your API key") - logger.error("2. Verify you have sufficient API credits") - logger.error("3. Check for rate limiting") - else: - logger.error("Troubleshooting tips:") - logger.error("1. Verify Ollama is running locally") - logger.error("2. Check if the requested model is installed") - raise - - total_time = time.time() - start_time - logger.info(f"\nPipeline completed in {total_time:.2f} seconds") - - except Exception as e: - logger.error(f"Pipeline failed: {str(e)}") - logger.debug("Stack trace:", exc_info=True) - sys.exit(1) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/src/skidl/scripts/llm_analysis/README.md b/src/skidl/scripts/llm_analysis/README.md new file mode 100644 index 000000000..d54dc357c --- /dev/null +++ b/src/skidl/scripts/llm_analysis/README.md @@ -0,0 +1,192 @@ +# KiCad/SKiDL LLM Analysis Package + +This package provides functionality for analyzing electronic circuit designs using Large Language Models (LLMs). It supports analyzing KiCad schematics, netlists, and SKiDL Python files with powerful AI-driven insights. + +## Overview + +The package is organized into several modules, each with a specific responsibility: + +``` +llm_analysis/ +├── __init__.py # Package initialization and public interface +├── cli.py # Command-line interface and argument handling +├── config.py # Configuration settings and constants +├── generator.py # Netlist and SKiDL project generation +├── kicad.py # KiCad integration utilities +├── logging.py # Logging configuration +├── state.py # Analysis state management +├── analyzer.py # Core circuit analysis functionality +└── prompts/ # LLM prompt templates + ├── __init__.py + ├── base.py # Base analysis prompt structure + └── sections.py # Individual analysis section templates +``` + +## Module Details + +### cli.py +- Main entry point and command-line interface +- Handles argument parsing and validation +- Orchestrates the analysis pipeline +- Key Functions: + * `parse_args()`: Command-line argument parsing + * `validate_args()`: Input validation + * `main()`: Pipeline orchestration + +### config.py +- Configuration constants and enums +- Defines LLM backends and defaults +- Constants: + * `DEFAULT_TIMEOUT`: LLM request timeout + * `DEFAULT_MODEL`: Default LLM model + * `Backend`: Enum of supported LLM backends + +### generator.py +- Handles file generation and conversion +- Key Functions: + * `generate_netlist()`: KiCad schematic to netlist conversion + * `generate_skidl_project()`: Netlist to SKiDL project conversion + * `get_skidl_source()`: Source resolution logic + +### kicad.py +- KiCad integration utilities +- Handles platform-specific paths +- Library management +- Key Functions: + * `validate_kicad_cli()`: CLI executable validation + * `get_default_kicad_cli()`: Platform-specific defaults + * `handle_kicad_libraries()`: Library path management + +### logging.py +- Logging configuration and utilities +- Custom formatters and handlers +- Key Functions: + * `configure_logging()`: Logger setup + * `log_analysis_results()`: Results formatting + * `log_backend_help()`: Backend-specific troubleshooting + +### state.py +- Thread-safe analysis state management +- Persistence support +- Key Class: `AnalysisState` + * Tracks completed/failed analyses + * Manages results + * Supports save/load for resumable analysis + +### analyzer.py +- Core analysis functionality +- Parallel processing support +- Key Functions: + * `analyze_single_circuit()`: Individual circuit analysis + * `analyze_circuits()`: Parallel analysis orchestration + +### prompts/ +- LLM prompt templates and structure +- Modular analysis sections +- Files: + * `base.py`: Base prompt structure + * `sections.py`: Analysis section templates + +## Dependencies + +- **skidl**: Core circuit processing functionality +- **KiCad**: Required for schematic/netlist operations +- **OpenRouter/Ollama**: LLM backends for analysis + +## Usage + +1. **Direct Command Line Usage**: +```bash +kicad_skidl_llm --schematic circuit.kicad_sch --generate-netlist --generate-skidl --analyze +``` + +2. **API Usage**: +```python +from skidl.scripts.llm_analysis import analyze_circuits, Backend + +results = analyze_circuits( + source=source_path, + api_key="your-api-key", + backend=Backend.OPENROUTER +) +``` + +## Data Flow + +1. Input Processing: + - Schematic → Netlist → SKiDL Project (optional) + - Direct SKiDL file/project input + +2. Analysis Pipeline: + ``` + Input → Circuit Loading → Parallel Analysis → Results Collection → Report Generation + ``` + +3. State Management: + - Thread-safe tracking + - Persistent state (optional) + - Progress monitoring + +## Threading Model + +- Parallel circuit analysis using ThreadPoolExecutor +- Thread-safe state management via locks +- Configurable concurrency limits + +## Error Handling + +- Platform-specific guidance +- Backend-specific troubleshooting +- Detailed error reporting +- State persistence for recovery + +## Configuration + +- Environment Variables: + * `KICAD_SYMBOL_DIR`: KiCad library path + * `OPENROUTER_API_KEY`: API key for OpenRouter + +- Command Line Options: + * Backend selection + * Model selection + * Debug levels + * Concurrent analysis limits + +## Development + +### Adding Support for New LLM Backends + +1. Add backend to `Backend` enum in `config.py` +2. Implement backend-specific error handling +3. Update `analyzer.py` with backend-specific logic + +### Adding New Analysis Sections + +1. Add section template to `prompts/sections.py` +2. Update `ANALYSIS_SECTIONS` dictionary +3. Update base prompt if needed + +## Common Issues + +1. **KiCad CLI Not Found**: + - Check PATH + - Verify KiCad installation + - Use `--kicad-cli` to specify path + +2. **Library Path Issues**: + - Set KICAD_SYMBOL_DIR + - Use `--kicad-lib-paths` + - Check library file existence + +3. **LLM Backend Issues**: + - Verify API key + - Check credits/rate limits + - Confirm backend availability + +## Future Improvements + +1. Additional LLM backends +2. More analysis section templates +3. Enhanced parallel processing +4. Interactive analysis modes +5. Result visualization \ No newline at end of file From c052c8133507557abad73b15b6d1fc9869808263 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 15:52:46 -0800 Subject: [PATCH 71/73] improve documentation --- src/skidl/scripts/llm_analysis/README.md | 301 +++++++++++------------ 1 file changed, 142 insertions(+), 159 deletions(-) diff --git a/src/skidl/scripts/llm_analysis/README.md b/src/skidl/scripts/llm_analysis/README.md index d54dc357c..de6e39949 100644 --- a/src/skidl/scripts/llm_analysis/README.md +++ b/src/skidl/scripts/llm_analysis/README.md @@ -2,191 +2,174 @@ This package provides functionality for analyzing electronic circuit designs using Large Language Models (LLMs). It supports analyzing KiCad schematics, netlists, and SKiDL Python files with powerful AI-driven insights. -## Overview +## Command-Line Usage -The package is organized into several modules, each with a specific responsibility: +### Basic Usage -``` -llm_analysis/ -├── __init__.py # Package initialization and public interface -├── cli.py # Command-line interface and argument handling -├── config.py # Configuration settings and constants -├── generator.py # Netlist and SKiDL project generation -├── kicad.py # KiCad integration utilities -├── logging.py # Logging configuration -├── state.py # Analysis state management -├── analyzer.py # Core circuit analysis functionality -└── prompts/ # LLM prompt templates - ├── __init__.py - ├── base.py # Base analysis prompt structure - └── sections.py # Individual analysis section templates +```bash +kicad_skidl_llm [input source] [operations] [options] ``` -## Module Details +### Input Sources (Required, Choose One) -### cli.py -- Main entry point and command-line interface -- Handles argument parsing and validation -- Orchestrates the analysis pipeline -- Key Functions: - * `parse_args()`: Command-line argument parsing - * `validate_args()`: Input validation - * `main()`: Pipeline orchestration - -### config.py -- Configuration constants and enums -- Defines LLM backends and defaults -- Constants: - * `DEFAULT_TIMEOUT`: LLM request timeout - * `DEFAULT_MODEL`: Default LLM model - * `Backend`: Enum of supported LLM backends - -### generator.py -- Handles file generation and conversion -- Key Functions: - * `generate_netlist()`: KiCad schematic to netlist conversion - * `generate_skidl_project()`: Netlist to SKiDL project conversion - * `get_skidl_source()`: Source resolution logic - -### kicad.py -- KiCad integration utilities -- Handles platform-specific paths -- Library management -- Key Functions: - * `validate_kicad_cli()`: CLI executable validation - * `get_default_kicad_cli()`: Platform-specific defaults - * `handle_kicad_libraries()`: Library path management - -### logging.py -- Logging configuration and utilities -- Custom formatters and handlers -- Key Functions: - * `configure_logging()`: Logger setup - * `log_analysis_results()`: Results formatting - * `log_backend_help()`: Backend-specific troubleshooting - -### state.py -- Thread-safe analysis state management -- Persistence support -- Key Class: `AnalysisState` - * Tracks completed/failed analyses - * Manages results - * Supports save/load for resumable analysis - -### analyzer.py -- Core analysis functionality -- Parallel processing support -- Key Functions: - * `analyze_single_circuit()`: Individual circuit analysis - * `analyze_circuits()`: Parallel analysis orchestration - -### prompts/ -- LLM prompt templates and structure -- Modular analysis sections -- Files: - * `base.py`: Base prompt structure - * `sections.py`: Analysis section templates - -## Dependencies - -- **skidl**: Core circuit processing functionality -- **KiCad**: Required for schematic/netlist operations -- **OpenRouter/Ollama**: LLM backends for analysis - -## Usage - -1. **Direct Command Line Usage**: -```bash -kicad_skidl_llm --schematic circuit.kicad_sch --generate-netlist --generate-skidl --analyze -``` +* `--schematic`, `-s` PATH + - Path to KiCad schematic (.kicad_sch) file + - Example: `--schematic project.kicad_sch` -2. **API Usage**: -```python -from skidl.scripts.llm_analysis import analyze_circuits, Backend +* `--netlist`, `-n` PATH + - Path to netlist (.net) file + - Example: `--netlist project.net` -results = analyze_circuits( - source=source_path, - api_key="your-api-key", - backend=Backend.OPENROUTER -) -``` +* `--skidl` PATH + - Path to SKiDL Python file to analyze + - Example: `--skidl circuit.py` + +* `--skidl-dir` PATH + - Path to SKiDL project directory + - Example: `--skidl-dir ./project_skidl` + +### Operations -## Data Flow +* `--generate-netlist` + - Generate netlist from schematic + - Requires `--schematic` -1. Input Processing: - - Schematic → Netlist → SKiDL Project (optional) - - Direct SKiDL file/project input +* `--generate-skidl` + - Generate SKiDL project from netlist + - Requires either `--netlist` or `--generate-netlist` -2. Analysis Pipeline: - ``` - Input → Circuit Loading → Parallel Analysis → Results Collection → Report Generation - ``` +* `--analyze` + - Run LLM analysis on circuits + - Can be used with any input source -3. State Management: - - Thread-safe tracking - - Persistent state (optional) - - Progress monitoring +### Analysis Options -## Threading Model +* `--backend` {openrouter, ollama} + - LLM backend to use (default: openrouter) + - Example: `--backend ollama` -- Parallel circuit analysis using ThreadPoolExecutor -- Thread-safe state management via locks -- Configurable concurrency limits +* `--api-key` KEY + - OpenRouter API key (required for OpenRouter backend) + - Example: `--api-key your-api-key` -## Error Handling +* `--model` MODEL + - Specific LLM model to use + - Default: google/gemini-2.0-flash-001 + - Example: `--model gpt-4` -- Platform-specific guidance -- Backend-specific troubleshooting -- Detailed error reporting -- State persistence for recovery +* `--analysis-prompt` PROMPT + - Custom prompt for circuit analysis + - Example: `--analysis-prompt "Focus on power distribution"` -## Configuration +* `--skip-circuits` LIST + - Comma-separated list of circuits to skip + - Example: `--skip-circuits "power_reg,usb_interface"` -- Environment Variables: - * `KICAD_SYMBOL_DIR`: KiCad library path - * `OPENROUTER_API_KEY`: API key for OpenRouter +* `--max-concurrent` N + - Maximum number of concurrent LLM analyses + - Default: 4 + - Example: `--max-concurrent 8` -- Command Line Options: - * Backend selection - * Model selection - * Debug levels - * Concurrent analysis limits +### Output Options -## Development +* `--output-dir`, `-o` DIR + - Output directory for generated files + - Default: current directory + - Example: `--output-dir ./output` -### Adding Support for New LLM Backends +* `--analysis-output` FILE + - Output file for analysis results + - Default: circuit_analysis.txt + - Example: `--analysis-output results.txt` -1. Add backend to `Backend` enum in `config.py` -2. Implement backend-specific error handling -3. Update `analyzer.py` with backend-specific logic +### KiCad Configuration -### Adding New Analysis Sections +* `--kicad-cli` PATH + - Path to kicad-cli executable + - Default: Platform-specific default path + - Example: `--kicad-cli /usr/local/bin/kicad-cli` -1. Add section template to `prompts/sections.py` -2. Update `ANALYSIS_SECTIONS` dictionary -3. Update base prompt if needed +* `--kicad-lib-paths` [PATHS...] + - List of custom KiCad library paths + - Example: `--kicad-lib-paths ~/kicad/libs /opt/kicad/libs` -## Common Issues +### Debug Options -1. **KiCad CLI Not Found**: - - Check PATH - - Verify KiCad installation - - Use `--kicad-cli` to specify path +* `--debug`, `-d` [LEVEL] + - Print debugging info + - Higher LEVEL means more info + - Example: `--debug 2` -2. **Library Path Issues**: - - Set KICAD_SYMBOL_DIR - - Use `--kicad-lib-paths` - - Check library file existence +### Example Commands -3. **LLM Backend Issues**: - - Verify API key - - Check credits/rate limits - - Confirm backend availability +1. Basic Analysis of KiCad Schematic: +```bash +kicad_skidl_llm \ + --schematic project.kicad_sch \ + --analyze \ + --api-key $OPENROUTER_API_KEY +``` + +2. Complete Pipeline with Custom Options: +```bash +kicad_skidl_llm \ + --schematic project.kicad_sch \ + --generate-netlist \ + --generate-skidl \ + --analyze \ + --backend openrouter \ + --api-key $OPENROUTER_API_KEY \ + --model gpt-4 \ + --max-concurrent 8 \ + --output-dir ./analysis \ + --analysis-output results.txt \ + --kicad-lib-paths ~/kicad/libs +``` + +3. Analyze Existing SKiDL Project: +```bash +kicad_skidl_llm \ + --skidl-dir ./project_skidl \ + --analyze \ + --api-key $OPENROUTER_API_KEY \ + --analysis-prompt "Focus on signal integrity" +``` -## Future Improvements +4. Local Analysis with Ollama: +```bash +kicad_skidl_llm \ + --skidl circuit.py \ + --analyze \ + --backend ollama \ + --model llama2 +``` + +## Environment Setup + +### Required Environment Variables + +1. For OpenRouter Backend: +```bash +export OPENROUTER_API_KEY="your-api-key" +``` + +2. For KiCad Integration: +```bash +export KICAD_SYMBOL_DIR="/path/to/kicad/symbols" +``` + +### Backend Requirements + +1. OpenRouter Backend: +- Valid API key +- Internet connection +- Sufficient API credits + +2. Ollama Backend: +- Local Ollama installation +- Required models installed +- Run `ollama pull model-name` to install models + +## Module Details -1. Additional LLM backends -2. More analysis section templates -3. Enhanced parallel processing -4. Interactive analysis modes -5. Result visualization \ No newline at end of file +[Rest of the original README content follows...] \ No newline at end of file From 4bbd18e28308f2a142a1d67eab492201acb53154 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 17:23:53 -0800 Subject: [PATCH 72/73] Add back README content --- src/skidl/scripts/llm_analysis/README.md | 157 ++++++++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/src/skidl/scripts/llm_analysis/README.md b/src/skidl/scripts/llm_analysis/README.md index de6e39949..cb8aeb177 100644 --- a/src/skidl/scripts/llm_analysis/README.md +++ b/src/skidl/scripts/llm_analysis/README.md @@ -170,6 +170,161 @@ export KICAD_SYMBOL_DIR="/path/to/kicad/symbols" - Required models installed - Run `ollama pull model-name` to install models +## Package Overview + +The package is organized into several modules, each with a specific responsibility: + +``` +llm_analysis/ +├── __init__.py # Package initialization and public interface +├── cli.py # Command-line interface and argument handling +├── config.py # Configuration settings and constants +├── generator.py # Netlist and SKiDL project generation +├── kicad.py # KiCad integration utilities +├── logging.py # Logging configuration +├── state.py # Analysis state management +├── analyzer.py # Core circuit analysis functionality +└── prompts/ # LLM prompt templates + ├── __init__.py + ├── base.py # Base analysis prompt structure + └── sections.py # Individual analysis section templates +``` + ## Module Details -[Rest of the original README content follows...] \ No newline at end of file +### cli.py +- Main entry point and command-line interface +- Handles argument parsing and validation +- Orchestrates the analysis pipeline +- Key Functions: + * `parse_args()`: Command-line argument parsing + * `validate_args()`: Input validation + * `main()`: Pipeline orchestration + +### config.py +- Configuration constants and enums +- Defines LLM backends and defaults +- Constants: + * `DEFAULT_TIMEOUT`: LLM request timeout + * `DEFAULT_MODEL`: Default LLM model + * `Backend`: Enum of supported LLM backends + +### generator.py +- Handles file generation and conversion +- Key Functions: + * `generate_netlist()`: KiCad schematic to netlist conversion + * `generate_skidl_project()`: Netlist to SKiDL project conversion + * `get_skidl_source()`: Source resolution logic + +### kicad.py +- KiCad integration utilities +- Handles platform-specific paths +- Library management +- Key Functions: + * `validate_kicad_cli()`: CLI executable validation + * `get_default_kicad_cli()`: Platform-specific defaults + * `handle_kicad_libraries()`: Library path management + +### logging.py +- Logging configuration and utilities +- Custom formatters and handlers +- Key Functions: + * `configure_logging()`: Logger setup + * `log_analysis_results()`: Results formatting + * `log_backend_help()`: Backend-specific troubleshooting + +### state.py +- Thread-safe analysis state management +- Persistence support +- Key Class: `AnalysisState` + * Tracks completed/failed analyses + * Manages results + * Supports save/load for resumable analysis + +### analyzer.py +- Core analysis functionality +- Parallel processing support +- Key Functions: + * `analyze_single_circuit()`: Individual circuit analysis + * `analyze_circuits()`: Parallel analysis orchestration + +### prompts/ +- LLM prompt templates and structure +- Modular analysis sections +- Files: + * `base.py`: Base prompt structure + * `sections.py`: Analysis section templates + +## Dependencies + +- **skidl**: Core circuit processing functionality +- **KiCad**: Required for schematic/netlist operations +- **OpenRouter/Ollama**: LLM backends for analysis + +## Data Flow + +1. Input Processing: + - Schematic → Netlist → SKiDL Project (optional) + - Direct SKiDL file/project input + +2. Analysis Pipeline: + ``` + Input → Circuit Loading → Parallel Analysis → Results Collection → Report Generation + ``` + +3. State Management: + - Thread-safe tracking + - Persistent state (optional) + - Progress monitoring + +## Threading Model + +- Parallel circuit analysis using ThreadPoolExecutor +- Thread-safe state management via locks +- Configurable concurrency limits + +## Error Handling + +- Platform-specific guidance +- Backend-specific troubleshooting +- Detailed error reporting +- State persistence for recovery + +## Development + +### Adding Support for New LLM Backends + +1. Add backend to `Backend` enum in `config.py` +2. Implement backend-specific error handling +3. Update `analyzer.py` with backend-specific logic + +### Adding New Analysis Sections + +1. Add section template to `prompts/sections.py` +2. Update `ANALYSIS_SECTIONS` dictionary +3. Update base prompt if needed + +## Common Issues + +1. **KiCad CLI Not Found**: + - Check PATH + - Verify KiCad installation + - Use `--kicad-cli` to specify path + +2. **Library Path Issues**: + - Set KICAD_SYMBOL_DIR + - Use `--kicad-lib-paths` + - Check library file existence + +3. **LLM Backend Issues**: + - Verify API key + - Check credits/rate limits + - Confirm backend availability + +## Future Improvements + +1. Additional LLM backends +2. More analysis section templates +3. Enhanced parallel processing +4. Interactive analysis modes +5. Result visualization \ No newline at end of file From 87c5bbd7d43a7bbc51ac32e4d38b6c79d2caf9b3 Mon Sep 17 00:00:00 2001 From: Shane Mattner Date: Fri, 7 Feb 2025 17:28:11 -0800 Subject: [PATCH 73/73] add parsing for purpose field for components --- src/skidl/circuit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/skidl/circuit.py b/src/skidl/circuit.py index e58a98ec9..eafdfe2b8 100644 --- a/src/skidl/circuit.py +++ b/src/skidl/circuit.py @@ -1282,6 +1282,9 @@ def get_circuit_info(self, hierarchy=None, depth=None, filename="circuit_descrip # Add part docstring if available if hasattr(part, 'description'): circuit_info.append(f" Description: {part.description}") + # Add part purpose if available + if hasattr(part, 'purpose'): + circuit_info.append(f" Purpose: {part.purpose}") circuit_info.append(" Pins:") for pin in part.pins: net_name = pin.net.name if pin.net else "unconnected"