Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ cython_debug/
.DS_Store
*Thumbs.db


# Possible generated files
*.cmd
*.tcl
Expand All @@ -180,3 +181,5 @@ cython_debug/
*.BAK
*.sav
*.plt
.virtual_documents
.idea
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gplugins_spice
19 changes: 19 additions & 0 deletions gplugins/hdl21/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from .netlist import (
ParsedProtoVLSIR,
generate_raw_netlist_dict_from_module,
generate_raw_yaml_from_module,
)
from .sky130 import (
filter_port,
find_most_relevant_gds,
hdl21_module_to_schematic_editor,
)

__all__ = [
"filter_port",
"find_most_relevant_gds",
"hdl21_module_to_schematic_editor",
"generate_raw_yaml_from_module",
"generate_raw_netlist_dict_from_module",
"ParsedProtoVLSIR",
]
232 changes: 232 additions & 0 deletions gplugins/hdl21/netlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
"""
This module provides functions to generate a raw netlist semi-compatible with gdsfactory from a hdl21 module object.
"""

import hdl21 as h
import yaml

ParsedProtoVLSIR = dict


def _parse_module_to_proto_dict(module: h.module) -> ParsedProtoVLSIR:
"""
Parse a hdl21 module object into a dictionary with the same structure as the proto VLSIR format.
"""

def parse_value(lines, index):
value = {}
while index < len(lines):
line = lines[index].strip()
if line == "}":
return value, index
elif line.endswith("{"):
key = line[:-1].strip()
sub_value, new_index = parse_value(lines, index + 1)
if key not in value:
value[key] = []
value[key].append(sub_value)
index = new_index
else:
key, val = line.split(":", 1)
value[key.strip()] = val.strip().strip('"')
index += 1
return value, index

raw_proto_str = str(h.to_proto(module))
lines = raw_proto_str.split("\n")
result = {}
index = 0
while index < len(lines):
line = lines[index].strip()
if line.endswith("{"):
key = line[:-1].strip()
sub_value, new_index = parse_value(lines, index + 1)
if key not in result:
result[key] = []
result[key].append(sub_value)
index = new_index
else:
index += 1

return result


def _parse_connections(proto_dict: ParsedProtoVLSIR) -> dict:
"""
Extract the connections from the proto_dict and return a dictionary with the connections.
"""
connections = {}

# Extract the instances and their connections
for module in proto_dict.get("modules", []):
for instance in module.get("instances", []):
instance_name = instance["name"]
for connection in instance.get("connections", []):
portname = connection["portname"]
target_signal = connection["target"][0]["sig"]
connection_key = f"{instance_name},{portname}"
# Find the target instance and port
target_instance_port = _find_target_instance_port(
proto_dict, target_signal, instance_name
)
if target_instance_port:
connections[connection_key] = target_instance_port

return connections


def _find_target_instance_port(
proto_dict: ParsedProtoVLSIR, target_signal, current_instance_name
):
"""
Find the target instance and port of the target signal in the proto_dict.
"""
# Search in the same module
for module in proto_dict.get("modules", []):
for instance in module.get("instances", []):
if instance["name"] == current_instance_name:
continue
for connection in instance.get("connections", []):
if connection["target"][0]["sig"] == target_signal:
return f"{instance['name']},{connection['portname']}"
# Search in external modules
for ext_module in proto_dict.get("ext_modules", []):
for port in ext_module.get("ports", []):
if port["signal"] == target_signal:
for instance in module.get("instances", []):
if instance["name"] == current_instance_name:
continue
for connection in instance.get("connections", []):
if connection["target"][0]["sig"] == target_signal:
return f"{instance['name']},{connection['portname']}"

return None


def _generate_top_level_connections(proto_dict: ParsedProtoVLSIR):
"""
Generate the top-level connections from the proto_dict.
"""
top_level_connections = {}

# Iterate over the top-level module ports
for module in proto_dict.get("modules", []):
for port in module.get("ports", []):
port_signal = port["signal"]
connection = _find_port_connection(proto_dict, port_signal)
if connection:
top_level_connections[port_signal] = connection

return top_level_connections


def _find_port_connection(proto_dict: ParsedProtoVLSIR, port_signal):
"""
Find the connection of the port signal in the proto_dict.
"""
# Search within the module instances
for module in proto_dict.get("modules", []):
for instance in module.get("instances", []):
instance_name = instance["name"]
for connection in instance.get("connections", []):
if connection["target"][0]["sig"] == port_signal:
return f"{instance_name},{connection['portname']}"
return None


def _extract_instance_parameters(proto_dict: ParsedProtoVLSIR):
"""
Extract the instance parameters from the proto_dict.
"""
instance_parameters = {}

for module in proto_dict.get("modules", []):
for instance in module.get("instances", []):
instance_name = instance["name"]
instance_info = {
"component": _extract_component_name(instance),
"info": {},
"settings": {},
}

# Extract parameters into the settings
for parameter in instance.get("parameters", []):
param_name = parameter["name"]
param_value = _extract_parameter_value(parameter["value"])
instance_info["settings"][param_name] = param_value

# Extract connections and add to settings
instance_info["settings"]["ports"] = {}
for connection in instance.get("connections", []):
portname = connection["portname"]
target_signal = connection["target"][0]["sig"]
instance_info["settings"]["ports"][portname] = target_signal

instance_parameters[instance_name] = instance_info

return instance_parameters


def _extract_component_name(instance):
"""
Extract the component name from the instance.
"""
external_modules = instance.get("module", [])
if external_modules:
name = external_modules[0].get("external", [{}])[0].get("name", "")
return f"{name}"
return "unknown_component"


def _extract_parameter_value(value):
"""
Extract the parameter value from the value dictionary.
"""
if value and "literal" in value[0]:
return value[0]["literal"]
elif value and "prefixed" in value[0]:
prefix = value[0]["prefixed"][0].get("prefix", "")
int64_value = value[0]["prefixed"][0].get("int64_value", "")
return f"{prefix}_{int64_value}"
return None


def _generate_raw_netlist_dict_from_proto_dict(proto_dict: ParsedProtoVLSIR):
"""
Generate a raw netlist dictionary from the proto_dict.
"""
raw_netlist_dict = {"name": "", "instances": {}, "connections": {}, "ports": {}}

# Extract the top-level module name
if proto_dict.get("modules"):
raw_netlist_dict["name"] = proto_dict["modules"][0].get("name", "")

# Generate instances information
raw_netlist_dict["instances"] = _extract_instance_parameters(proto_dict)

# Generate connections
raw_netlist_dict["connections"] = _parse_connections(proto_dict)

# Generate top-level connections
raw_netlist_dict["ports"] = _generate_top_level_connections(proto_dict)

return raw_netlist_dict


def generate_raw_netlist_dict_from_module(module: h.module):
"""
Generate a raw netlist dictionary from a hdl21 module object.
This just gives us a raw structure of the hdl21 modules, we cannot use this json equivalently to a gdsfactory netlist.
"""
proto_dict = _parse_module_to_proto_dict(module)
return _generate_raw_netlist_dict_from_proto_dict(proto_dict)


def generate_raw_yaml_from_module(module: h.module):
"""
Generate a raw netlist yaml from a hdl21 module object which could be manually edited for specific instances
related to the corresponding SPICE.
"""

raw_netlist = generate_raw_netlist_dict_from_module(module)
return yaml.dump(raw_netlist, default_flow_style=False)
95 changes: 95 additions & 0 deletions gplugins/hdl21/sky130.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from collections.abc import Callable
from difflib import get_close_matches

import hdl21 as h
import sky130

from ..schematic_editor import SchematicEditor
from .netlist import (
_generate_raw_netlist_dict_from_proto_dict,
_parse_module_to_proto_dict,
)

custom_mapping_dict = {
"sky130_fd_pr__nfet_01v8": "sky130_fd_pr__rf_nfet_01v8_aM02W1p65L0p15",
"sky130_fd_pr__pfet_01v8": "sky130_fd_pr__rf_pfet_01v8_mcM04W3p00L0p15",
}


def find_most_relevant_gds(
component_name, component_dict=sky130.cells, custom_mapping=None
):
if custom_mapping is None:
custom_mapping = custom_mapping_dict

if component_name in custom_mapping.keys():
print(f"Mapping for {component_name}: {custom_mapping[component_name]}")
return custom_mapping[component_name]

all_components = [
name for name in component_dict.keys() if "rf_test_coil" not in name
]
closest_matches = get_close_matches(component_name, all_components, n=1, cutoff=0.1)
print(f"Closest matches for {component_name}: {closest_matches}")
return closest_matches[0] if closest_matches else component_name


def filter_port(port):
"""
Filter the port name to match spice declaration to gds port name, specifically focused on the SKY130nm technology.
"""
if port == "d":
return "DRAIN"
elif port == "g":
return "GATE"
elif port == "s":
return "SOURCE"
else:
return port


def hdl21_module_to_schematic_editor(
module: h.module,
yaml_schematic_file_name: str,
spice_gds_mapping_method: Callable | None = find_most_relevant_gds,
port_filter_method: Callable = filter_port,
) -> SchematicEditor:
"""
Constructs a SchematicEditor instance from a hdl21 module object.

Args:
module (h.module): The hdl21 module object.
yaml_schematic_file_name (str): The yaml schematic file name.
spice_gds_mapping_method (Callable): The method to map the spice instance name to the component name.
port_filter_method (Callable): The method to filter the port name.
"""
proto_dict = _parse_module_to_proto_dict(module)
raw_netlist_dict = _generate_raw_netlist_dict_from_proto_dict(proto_dict)

# This just gives us a raw structure of the hdl21 modules.
se = SchematicEditor(yaml_schematic_file_name)

for instance_name_i, instance_i in raw_netlist_dict["instances"].items():
# Maps the spice instance name to the component name.
# TODO implement setting mapping and custom name mapping
if spice_gds_mapping_method is None:
gds_component_name_i = instance_i["component"]
else:
gds_component_name_i = spice_gds_mapping_method(
instance_i["component"], sky130.cells
)
se.add_instance(
instance_name=instance_name_i,
component=sky130.cells[gds_component_name_i](),
)

for connection_source_i, connection_target_i in raw_netlist_dict[
"connections"
].items():
source_instance, source_port = connection_source_i.split(",")
target_instance, target_port = connection_target_i.split(",")
source_port = port_filter_method(source_port)
target_port = port_filter_method(target_port)
se.add_net(source_instance, source_port, target_instance, target_port)

return se
17 changes: 10 additions & 7 deletions gplugins/schematic_editor/schematic_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,16 +327,19 @@ def update_settings(
instance_name=instance, settings=settings, old_settings=old_settings
)

def add_net(self, inst1, port1, inst2, port2):
def add_net(self, inst1, port1, inst2, port2, allow_multiple: bool = False) -> None:
p1 = f"{inst1},{port1}"
p2 = f"{inst2},{port2}"
if p1 in self._connected_ports:
if self._connected_ports[p1] == p2:
return
current_port = self._connected_ports[p1]
raise ValueError(
f"{p1} is already connected to {current_port}. Can't connect to {p2}"
)
if allow_multiple:
pass
else:
if self._connected_ports[p1] == p2:
return
current_port = self._connected_ports[p1]
raise ValueError(
f"{p1} is already connected to {current_port}. Can't connect to {p2}"
)
self._connected_ports[p1] = p2
self._connected_ports[p2] = p1
old_nets = self._schematic.nets.copy()
Expand Down
Loading