Skip to content

LLM analysis and combined Kicad > Netlist > SKIDL > LLM analysis logic #247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 77 commits into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
2f24379
add logic to print out circuit info
Jan 5, 2025
3183cfe
add test script
Jan 5, 2025
c3e1459
add circuit analyzer function
Jan 5, 2025
9f835db
logic to query anthropic added and working
Jan 5, 2025
6c53686
add llm analysis class
Jan 5, 2025
c58e260
add to circuit script to use new logic
Jan 5, 2025
77f87c2
artifacts from changes
Jan 5, 2025
6eb501c
modify chat completion script
Jan 5, 2025
9a6b4ff
Refactor prompt logic
Jan 5, 2025
6bd9d59
improved
Jan 5, 2025
b25edd8
improve prompt
Jan 5, 2025
945c8b2
add subcircuit analysis and queries
Jan 5, 2025
783004b
edit chat completion script
Jan 5, 2025
de929fd
refactor some logic
Jan 12, 2025
9826bda
update max tokens, delete unused files
Jan 12, 2025
6803f4e
refactor to use openrouter
Jan 12, 2025
b4a22be
refactor llm test script to include more info
Jan 12, 2025
9ff36d0
remove unneeded file
Jan 12, 2025
a72b3f1
increase max tokens
Jan 12, 2025
1f1fdb8
refactor to reduce new functions
Jan 12, 2025
f269c9a
refactor to simplify
Jan 12, 2025
8ec9da1
refactor
Jan 12, 2025
2997b80
remove unused file
Jan 12, 2025
45953a8
remove duplicate methods
Jan 14, 2025
1fb2099
add option to output prompt file instead of sending api calls
Jan 14, 2025
7fac50e
change sample script to output query to file
Jan 14, 2025
45dbdfc
add ability to add custom prompt for circuit analysis
Jan 14, 2025
277239d
refactor to display default model name
Jan 14, 2025
9251768
add back hierarchy nets
Jan 17, 2025
0506b55
fix: Fixed exception when save_query_only is enabled but unneeded bac…
Jan 18, 2025
316fe74
Merge branch 'shane/llm_analysis' of https://github.com/shanemmattner…
Jan 18, 2025
efc3d8e
Merge branch 'development' into llm
Jan 23, 2025
951f723
Merge branch 'development' into llm
Jan 31, 2025
0521527
somewhat working script to generate skidl project when pointed at a k…
Feb 1, 2025
818b930
update script
Feb 1, 2025
eb2dfd1
add return statement to all generated circuits, this prevents empty c…
Feb 1, 2025
3f59345
add fields for parts so we dont have invalid python syntax and retain…
Feb 1, 2025
9c28402
making progress towards generating nets properly
Feb 1, 2025
cc67f4f
generating nets almost properly
Feb 1, 2025
6cb2ee7
making progress!
Feb 1, 2025
a632de3
working net creation
Feb 1, 2025
e865a2d
remove prints, change some comments
Feb 1, 2025
34078ce
refactor kicad_skidl_llm.py script
Feb 2, 2025
b1ab6b7
refactor net building logic to look for top level to generate net
Feb 2, 2025
2e28bcb
generating nets properly
Feb 2, 2025
413cc01
improving algo
Feb 2, 2025
110673b
almost there
Feb 2, 2025
16373bc
logic somewhat working on some projects
Feb 2, 2025
2b75c0b
code is close to working
Feb 2, 2025
d42f337
getting closer, most of the circuits are correct
Feb 2, 2025
f16b277
replace logger statements with print statemnets
Feb 3, 2025
ea51841
fixed simple circuit logic
Feb 3, 2025
71462d8
hierarchies for simple and complex projects appear to be working well…
Feb 3, 2025
67892dd
refactor, working still
Feb 3, 2025
dbf5c0c
remove debug script
Feb 3, 2025
63fa615
working llm analysis again
Feb 3, 2025
acf720e
working ollama example
Feb 3, 2025
5ee7591
replace print with active_logger
Feb 3, 2025
4e531d3
refactor kicad_skidl_llm.py to be OS agnostic
Feb 3, 2025
ea5e880
add comments and other small refactoring
Feb 3, 2025
128d202
remove test script
Feb 3, 2025
c5379da
Merge branch 'development' into kicad_to_skidl_to_llm
Feb 4, 2025
86f1b66
add extra error handling if user is missing custom schematic library …
Feb 4, 2025
1e42842
add descriptive error message for not including custom library path
Feb 4, 2025
451030d
commiting current change
Feb 7, 2025
a895d73
update prompts to look for capacitance derating
Feb 7, 2025
a79b80d
update script to remove duplicate parsing and use skidl project defau…
Feb 7, 2025
4595212
add function to skip subcircuits
Feb 7, 2025
696c849
refactor shared logic
Feb 7, 2025
18b0173
make analysis llm calls parallel
Feb 7, 2025
c761172
add required openai pip install, change logic to look for KICAD_SYMBO…
shanemmattner Feb 7, 2025
46d3a33
moved kicad_skidl_llm script to folder
shanemmattner Feb 7, 2025
ba3852c
make smaller files for llm analysis feature
shanemmattner Feb 7, 2025
4110075
add readme, remove old file
shanemmattner Feb 7, 2025
c052c81
improve documentation
shanemmattner Feb 7, 2025
4bbd18e
Add back README content
shanemmattner Feb 8, 2025
87c5bbd
add parsing for purpose field for components
shanemmattner Feb 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,5 @@ uno_r3
*.bak
*.pkl

# Mac stuff
.DS_Store
8 changes: 6 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
"sexpdata == 1.0.0",
"kinparse >= 1.2.3",
"kinet2pcb >= 1.1.2",
#'PySpice; python_version >= "3.0"',
"graphviz",
"deprecation",
"requests >= 2.31.0",
"importlib-metadata", # For importlib support
"typing-extensions", # For type hints in Python <3.8
"openai",
]

test_requirements = [
Expand All @@ -59,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"},
Expand Down
3 changes: 3 additions & 0 deletions src/skidl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -57,6 +58,8 @@
generate_svg,
generate_xml,
lib_search_paths,
get_circuit_info,
analyze_with_llm,
no_files,
reset,
get_default_tool,
Expand Down
224 changes: 220 additions & 4 deletions src/skidl/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import json
import subprocess
from collections import Counter, deque
from collections import Counter, deque, defaultdict

import graphviz

Expand Down Expand Up @@ -106,13 +106,15 @@ 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 = (
[]
) # 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}
Expand Down Expand Up @@ -191,9 +193,20 @@ 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
builtins.NC = self.NC

def deactivate(self):
"""Deactivate the current hierarchical group and return to the previous one."""
Expand Down Expand Up @@ -1198,3 +1211,206 @@ 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, 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.
"""

# A list for storing lines of text describing the circuit.
circuit_info = []
circuit_info.append("=" * 40)
circuit_info.append(f"Circuit Name: {self.name}")

# Get hierarchy label for the starting point.
start_hier = hierarchy or self.hierarchy
start_depth = len(start_hier.split(HIER_SEP))
circuit_info.append(f"Starting Hierarchy: {start_hier}")

# 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(HIER_SEP)) - start_depth <= depth:
hierarchy_parts[part.hierarchy].append(part)
hierarchies.add(part.hierarchy)

# 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:
net_hier_connections[pin.part.hierarchy].append(pin)

for hier in net_hier_connections:
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}")
# 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"
circuit_info.append(f" {pin.num}/{pin.name}: {net_name}")

# 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)

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 (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.
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.
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:
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

if save_query_only:
backend = None # Don't need backend for query only.

analyzer = SkidlCircuitAnalyzer(
api_key=api_key,
custom_prompt=custom_prompt,
backend=backend,
model=model
)

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, save_query_only=save_query_only)

# 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
save_query_only=save_query_only
)

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
Loading