Skip to content

Commit 208d7a1

Browse files
committed
:sparkes: Beta demo sky130 spice-to-layout
1 parent 1d6d35b commit 208d7a1

25 files changed

+2021
-133
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ cython_debug/
172172
.DS_Store
173173
*Thumbs.db
174174

175+
175176
# Possible generated files
176177
*.cmd
177178
*.tcl
@@ -180,3 +181,5 @@ cython_debug/
180181
*.BAK
181182
*.sav
182183
*.plt
184+
.virtual_documents
185+
.idea

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
gplugins_spice

gplugins/hdl21/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .netlist import *
2+
from .sky130 import *

gplugins/hdl21/netlist.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"""
2+
This module provides functions to generate a raw netlist semi-compatible with gdsfactory from a hdl21 module object.
3+
"""
4+
import yaml
5+
import hdl21 as h
6+
7+
__all__ = [
8+
'ParsedProtoVLSIR',
9+
'generate_raw_netlist_dict_from_module',
10+
'generate_raw_yaml_from_module'
11+
]
12+
13+
ParsedProtoVLSIR = dict
14+
15+
16+
def _parse_module_to_proto_dict(module: h.module) -> ParsedProtoVLSIR:
17+
"""
18+
Parse a hdl21 module object into a dictionary with the same structure as the proto VLSIR format.
19+
"""
20+
21+
def parse_value(lines, index):
22+
value = {}
23+
while index < len(lines):
24+
line = lines[index].strip()
25+
if line == "}":
26+
return value, index
27+
elif line.endswith("{"):
28+
key = line[:-1].strip()
29+
sub_value, new_index = parse_value(lines, index + 1)
30+
if key not in value:
31+
value[key] = []
32+
value[key].append(sub_value)
33+
index = new_index
34+
else:
35+
key, val = line.split(":", 1)
36+
value[key.strip()] = val.strip().strip('"')
37+
index += 1
38+
return value, index
39+
40+
raw_proto_str = str(h.to_proto(module))
41+
lines = raw_proto_str.split("\n")
42+
result = {}
43+
index = 0
44+
while index < len(lines):
45+
line = lines[index].strip()
46+
if line.endswith("{"):
47+
key = line[:-1].strip()
48+
sub_value, new_index = parse_value(lines, index + 1)
49+
if key not in result:
50+
result[key] = []
51+
result[key].append(sub_value)
52+
index = new_index
53+
else:
54+
index += 1
55+
56+
return result
57+
58+
59+
def _parse_connections(proto_dict: ParsedProtoVLSIR) -> dict:
60+
"""
61+
Extract the connections from the proto_dict and return a dictionary with the connections.
62+
"""
63+
connections = {}
64+
65+
# Extract the instances and their connections
66+
for module in proto_dict.get('modules', []):
67+
for instance in module.get('instances', []):
68+
instance_name = instance['name']
69+
for connection in instance.get('connections', []):
70+
portname = connection['portname']
71+
target_signal = connection['target'][0]['sig']
72+
connection_key = f"{instance_name},{portname}"
73+
# Find the target instance and port
74+
target_instance_port = _find_target_instance_port(proto_dict, target_signal, instance_name)
75+
if target_instance_port:
76+
connections[connection_key] = target_instance_port
77+
78+
return connections
79+
80+
81+
def _find_target_instance_port(proto_dict: ParsedProtoVLSIR,
82+
target_signal,
83+
current_instance_name):
84+
"""
85+
Find the target instance and port of the target signal in the proto_dict.
86+
"""
87+
# Search in the same module
88+
for module in proto_dict.get('modules', []):
89+
for instance in module.get('instances', []):
90+
if instance['name'] == current_instance_name:
91+
continue
92+
for connection in instance.get('connections', []):
93+
if connection['target'][0]['sig'] == target_signal:
94+
return f"{instance['name']},{connection['portname']}"
95+
# Search in external modules
96+
for ext_module in proto_dict.get('ext_modules', []):
97+
for port in ext_module.get('ports', []):
98+
if port['signal'] == target_signal:
99+
for instance in module.get('instances', []):
100+
if instance['name'] == current_instance_name:
101+
continue
102+
for connection in instance.get('connections', []):
103+
if connection['target'][0]['sig'] == target_signal:
104+
return f"{instance['name']},{connection['portname']}"
105+
106+
return None
107+
108+
109+
def _generate_top_level_connections(proto_dict: ParsedProtoVLSIR):
110+
"""
111+
Generate the top-level connections from the proto_dict.
112+
"""
113+
top_level_connections = {}
114+
115+
# Iterate over the top-level module ports
116+
for module in proto_dict.get('modules', []):
117+
for port in module.get('ports', []):
118+
port_signal = port['signal']
119+
connection = _find_port_connection(proto_dict, port_signal)
120+
if connection:
121+
top_level_connections[port_signal] = connection
122+
123+
return top_level_connections
124+
125+
126+
def _find_port_connection(proto_dict: ParsedProtoVLSIR, port_signal):
127+
"""
128+
Find the connection of the port signal in the proto_dict.
129+
"""
130+
# Search within the module instances
131+
for module in proto_dict.get('modules', []):
132+
for instance in module.get('instances', []):
133+
instance_name = instance['name']
134+
for connection in instance.get('connections', []):
135+
if connection['target'][0]['sig'] == port_signal:
136+
return f"{instance_name},{connection['portname']}"
137+
return None
138+
139+
140+
def _extract_instance_parameters(proto_dict: ParsedProtoVLSIR):
141+
"""
142+
Extract the instance parameters from the proto_dict.
143+
"""
144+
instance_parameters = {}
145+
146+
for module in proto_dict.get('modules', []):
147+
for instance in module.get('instances', []):
148+
instance_name = instance['name']
149+
instance_info = {
150+
'component': _extract_component_name(instance),
151+
'info': {},
152+
'settings': {}
153+
}
154+
155+
# Extract parameters into the settings
156+
for parameter in instance.get('parameters', []):
157+
param_name = parameter['name']
158+
param_value = _extract_parameter_value(parameter['value'])
159+
instance_info['settings'][param_name] = param_value
160+
161+
# Extract connections and add to settings
162+
instance_info['settings']['ports'] = {}
163+
for connection in instance.get('connections', []):
164+
portname = connection['portname']
165+
target_signal = connection['target'][0]['sig']
166+
instance_info['settings']['ports'][portname] = target_signal
167+
168+
instance_parameters[instance_name] = instance_info
169+
170+
return instance_parameters
171+
172+
173+
def _extract_component_name(instance):
174+
"""
175+
Extract the component name from the instance.
176+
"""
177+
external_modules = instance.get('module', [])
178+
if external_modules:
179+
domain = external_modules[0].get('external', [{}])[0].get('domain', '')
180+
name = external_modules[0].get('external', [{}])[0].get('name', '')
181+
return f"{name}"
182+
return 'unknown_component'
183+
184+
185+
def _extract_parameter_value(value):
186+
"""
187+
Extract the parameter value from the value dictionary.
188+
"""
189+
if value and 'literal' in value[0]:
190+
return value[0]['literal']
191+
elif value and 'prefixed' in value[0]:
192+
prefix = value[0]['prefixed'][0].get('prefix', '')
193+
int64_value = value[0]['prefixed'][0].get('int64_value', '')
194+
return f"{prefix}_{int64_value}"
195+
return None
196+
197+
198+
def _generate_raw_netlist_dict_from_proto_dict(proto_dict: ParsedProtoVLSIR):
199+
"""
200+
Generate a raw netlist dictionary from the proto_dict.
201+
"""
202+
raw_netlist_dict = {
203+
'name': '',
204+
'instances': {},
205+
'connections': {},
206+
'ports': {}
207+
}
208+
209+
# Extract the top-level module name
210+
if proto_dict.get('modules'):
211+
raw_netlist_dict['name'] = proto_dict['modules'][0].get('name', '')
212+
213+
# Generate instances information
214+
raw_netlist_dict['instances'] = _extract_instance_parameters(proto_dict)
215+
216+
# Generate connections
217+
raw_netlist_dict['connections'] = _parse_connections(proto_dict)
218+
219+
# Generate top-level connections
220+
raw_netlist_dict['ports'] = _generate_top_level_connections(proto_dict)
221+
222+
return raw_netlist_dict
223+
224+
225+
def generate_raw_netlist_dict_from_module(module: h.module):
226+
"""
227+
Generate a raw netlist dictionary from a hdl21 module object.
228+
This just gives us a raw structure of the hdl21 modules, we cannot use this json equivalently to a gdsfactory netlist.
229+
"""
230+
proto_dict = _parse_module_to_proto_dict(module)
231+
return _generate_raw_netlist_dict_from_proto_dict(proto_dict)
232+
233+
234+
def generate_raw_yaml_from_module(module: h.module):
235+
"""
236+
Generate a raw netlist yaml from a hdl21 module object which could be manually edited for specific instances
237+
related to the corresponding SPICE.
238+
"""
239+
raw_netlist = generate_raw_netlist_dict_from_module(module)
240+
return yaml.dump(raw_netlist, default_flow_style=False)

gplugins/hdl21/sky130.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import sky130
2+
import hdl21 as h
3+
from typing import Callable, Optional
4+
from ..schematic_editor import SchematicEditor
5+
from .netlist import _parse_module_to_proto_dict, _generate_raw_netlist_dict_from_proto_dict
6+
7+
__all__ = [
8+
'hdl21_module_to_schematic_editor',
9+
"find_most_relevant_gds",
10+
"filter_port"
11+
]
12+
13+
from difflib import get_close_matches
14+
15+
custom_mapping_dict = {
16+
"sky130_fd_pr__nfet_01v8": "sky130_fd_pr__rf_nfet_01v8_aM02W1p65L0p15",
17+
"sky130_fd_pr__pfet_01v8": "sky130_fd_pr__rf_pfet_01v8_mcM04W3p00L0p15"
18+
}
19+
20+
21+
def find_most_relevant_gds(component_name,
22+
component_dict=sky130.cells,
23+
custom_mapping=None):
24+
25+
if custom_mapping is None:
26+
custom_mapping = custom_mapping_dict
27+
28+
if component_name in custom_mapping.keys():
29+
print(f"Mapping for {component_name}: {custom_mapping[component_name]}")
30+
return custom_mapping[component_name]
31+
32+
all_components = [name for name in component_dict.keys() if "rf_test_coil" not in name]
33+
closest_matches = get_close_matches(component_name, all_components, n=1, cutoff=0.1)
34+
print(f"Closest matches for {component_name}: {closest_matches}")
35+
return closest_matches[0] if closest_matches else component_name
36+
37+
38+
def filter_port(port):
39+
"""
40+
Filter the port name to match spice declaration to gds port name, specifically focused on the SKY130nm technology.
41+
"""
42+
if port == "d":
43+
return "DRAIN"
44+
elif port == "g":
45+
return "GATE"
46+
elif port == "s":
47+
return "SOURCE"
48+
else:
49+
return port
50+
51+
52+
def hdl21_module_to_schematic_editor(module: h.module,
53+
yaml_schematic_file_name: str,
54+
spice_gds_mapping_method: Optional[Callable] = find_most_relevant_gds,
55+
port_filter_method: Callable = filter_port
56+
) -> SchematicEditor:
57+
"""
58+
Constructs a SchematicEditor instance from a hdl21 module object.
59+
60+
Args:
61+
module (h.module): The hdl21 module object.
62+
yaml_schematic_file_name (str): The yaml schematic file name.
63+
spice_gds_mapping_method (Callable): The method to map the spice instance name to the component name.
64+
port_filter_method (Callable): The method to filter the port name.
65+
"""
66+
proto_dict = _parse_module_to_proto_dict(module)
67+
raw_netlist_dict = _generate_raw_netlist_dict_from_proto_dict(proto_dict)
68+
69+
# This just gives us a raw structure of the hdl21 modules.
70+
se = SchematicEditor(yaml_schematic_file_name)
71+
72+
for instance_name_i, instance_i in raw_netlist_dict["instances"].items():
73+
# Maps the spice instance name to the component name.
74+
# TODO implement setting mapping and custom name mapping
75+
if spice_gds_mapping_method is None:
76+
gds_component_name_i = instance_i["component"]
77+
else:
78+
gds_component_name_i = spice_gds_mapping_method(instance_i["component"], sky130.cells)
79+
se.add_instance(
80+
instance_name=instance_name_i,
81+
component=sky130.cells[gds_component_name_i](),
82+
)
83+
84+
for connection_source_i, connection_target_i in raw_netlist_dict["connections"].items():
85+
source_instance, source_port = connection_source_i.split(",")
86+
target_instance, target_port = connection_target_i.split(",")
87+
source_port = port_filter_method(source_port)
88+
target_port = port_filter_method(target_port)
89+
se.add_net(source_instance, source_port, target_instance, target_port)
90+
91+
return se

gplugins/schematic_editor/circuitviz.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ def viz_netlist(netlist, instances, instance_size=20):
455455

456456
for net in netlist.nets:
457457
p_in, p_out = net
458+
print(port_coords)
458459
point1 = port_coords[p_in]
459460
point2 = port_coords[p_out]
460461
els += viz_connection(netlist, p_in, p_out, instance_size, point1, point2)
@@ -466,6 +467,7 @@ def show_netlist(
466467
) -> None:
467468
global data
468469
data["netlist"] = schematic
470+
print(schematic)
469471
fig = bp.figure(width=800, height=500)
470472
app = viz_bk(
471473
schematic,
@@ -474,6 +476,7 @@ def show_netlist(
474476
instance_size=50,
475477
netlist_filename=netlist_filename,
476478
)
479+
print(app)
477480
bio.show(app)
478481

479482

0 commit comments

Comments
 (0)