472 lines
16 KiB
Python
472 lines
16 KiB
Python
import time
|
|
import nazca as nd
|
|
import json,yaml
|
|
import os,sys
|
|
import gdstk
|
|
|
|
from pathlib import Path
|
|
import cairosvg
|
|
import math
|
|
|
|
|
|
# ==============================================================================
|
|
# ANSI Escape Codes for Console Logging
|
|
# ==============================================================================
|
|
RED = "\033[1;31m"
|
|
GRE = "\033[1;32m"
|
|
YEL = "\033[1;33m"
|
|
BLU = "\033[1;34m"
|
|
PUR = "\033[1;35m"
|
|
CRY = "\033[1;36m"
|
|
ENC = "\033[0m"
|
|
|
|
|
|
|
|
def encode_string(s, alphabet="1234567890ABCDEFGHIJKLMNOPGRSTUVWXYZ"):
|
|
base = len(alphabet)
|
|
num = int.from_bytes(s.encode(), 'big')
|
|
|
|
result = ""
|
|
while num > 0:
|
|
num, rem = divmod(num, base)
|
|
result = alphabet[rem] + result
|
|
|
|
return result
|
|
|
|
def decode_string(encoded, alphabet):
|
|
base = len(alphabet)
|
|
|
|
num = 0
|
|
for c in encoded:
|
|
num = num * base + alphabet.index(c)
|
|
|
|
length = (num.bit_length() + 7) // 8
|
|
return num.to_bytes(length, 'big').decode()
|
|
|
|
|
|
def mx_pdk_gen(fname:str,cell,fpath=None,info=None,layer_exclude=None):
|
|
|
|
""" Disposing fname and fpath """
|
|
if (fpath[-1] != "\\"):
|
|
fpath = fpath + "\\"
|
|
|
|
if (fname.endswith(".gds")):
|
|
fname = fname.removesuffix(".gds")
|
|
|
|
# ## 1. adding information to the ports ##
|
|
# with nd.Cell(instantiate=False) as cell_with_info:
|
|
# inst = cell.put(0,0,0)
|
|
# for pinName,Pin in cell_with_info.ic_pins():
|
|
# fdict = {}
|
|
# fdict["x"] = Pin.x
|
|
# fdict["y"] = Pin.y
|
|
# fdict["a"] = Pin.a
|
|
# fdict["width"] = Pin.width
|
|
# fdict["name"] = pinName
|
|
# nd.put_parameters(parameters=fdict,pin=Pin)
|
|
# nd.put_stub(pinsize=4)
|
|
|
|
## 2. export gds files ##
|
|
nd.export_gds(filename=fpath+fname+".gds",topcells=cell)
|
|
|
|
## 3. creating other releated files including .yml, _BB.gds and .png files ##
|
|
pinFileGen(fname=fname,cell=cell,fpath=fpath,info=info, layer_exclude=layer_exclude)
|
|
|
|
|
|
def pinFileGen(fname, cell, fpath=None, info=None, layer_exclude=None):
|
|
fdict = {}
|
|
data_yml = {}
|
|
timeCurr = time.strftime("%Y%m%d-%H%M%S")
|
|
|
|
fdict["time"] = timeCurr
|
|
data_yml["ports"] = {}
|
|
|
|
for pinName, Pin in cell.ic_pins():
|
|
fdict[pinName] = {
|
|
"x": Pin.x,
|
|
"y": Pin.y,
|
|
"a": Pin.a,
|
|
"width": Pin.width
|
|
}
|
|
|
|
pin_type = "None" if Pin.type is None else Pin.type
|
|
pin_descript = "None"
|
|
|
|
if (Pin.type is not None ):
|
|
pin_descript = pin_type.split(":")[1]
|
|
pin_type = pin_type.split(":")[0]
|
|
|
|
|
|
data_yml["ports"][pinName] = {
|
|
"x": round(float(Pin.x), 3),
|
|
"y": round(float(Pin.y), 3),
|
|
"a": round(float(Pin.a), 3),
|
|
"width": "None" if Pin.width is None else round(float(Pin.width), 3),
|
|
"type": pin_type,
|
|
"description": pin_descript,
|
|
}
|
|
|
|
# Safely handle directory and paths using pathlib
|
|
output_dir = Path(fpath) if fpath else Path.cwd()
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
base_path = output_dir / fname
|
|
|
|
# File targets
|
|
orig_gds_path = base_path.with_suffix(".gds")
|
|
bb_gds_path = output_dir / f"{fname}_BB.gds"
|
|
json_path = base_path.with_suffix(".json")
|
|
yml_path = base_path.with_suffix(".yml")
|
|
|
|
orig_png_path = base_path.with_suffix(".svg")
|
|
bb_png_path = output_dir / f"{fname}_BB.svg"
|
|
|
|
file_size_kb = 0.0
|
|
box_sz = [0.0, 0.0]
|
|
|
|
## Generation of black box cell
|
|
try:
|
|
if orig_gds_path.exists():
|
|
file_size_bytes = os.path.getsize(orig_gds_path)
|
|
file_size_kb = round(float(file_size_bytes / 1024), 2)
|
|
|
|
# Your existing black box generator
|
|
box_sz = generate_bb_and_svgs(gds_name=str(orig_gds_path), pin_info=data_yml["ports"], fpath=str(output_dir),layer_exclude=layer_exclude)
|
|
# box_sz = BB_gds_gen(gds_name=str(orig_gds_path), pin_info=data_yml["ports"], fpath=str(output_dir))
|
|
print(f"{GRE} Black box GDS cell successfully generated for {YEL}{fname}{ENC}")
|
|
|
|
# Trigger PNG Generation for both Original and Black-Box
|
|
# if orig_gds_path.exists():
|
|
# render_gds_to_svg(orig_gds_path, orig_png_path)
|
|
|
|
# if bb_gds_path.exists():
|
|
# render_gds_to_svg(bb_gds_path, bb_png_path)
|
|
|
|
except Exception as e:
|
|
print(f"{RED} Error in Black box or PNG generation of {fname} in path {YEL}{output_dir}{ENC}. Details: {e}")
|
|
|
|
|
|
## Creating .json and .yml files
|
|
try:
|
|
with open(json_path, 'w') as f:
|
|
json.dump(fdict, f, indent=4)
|
|
|
|
if info is not None:
|
|
data_yml = {**info, **data_yml}
|
|
|
|
data_yml["time"] = timeCurr
|
|
|
|
if box_sz is not None and len(box_sz) >= 2:
|
|
data_yml["box_size"] = [round(float(box_sz[0]), 3), round(float(box_sz[1]), 3)]
|
|
else:
|
|
data_yml["box_size"] = [0.0, 0.0]
|
|
|
|
data_yml["file_size"] = f"{file_size_kb} KB"
|
|
|
|
with open(yml_path, mode="w") as Fyml:
|
|
yaml.dump(data=data_yml, stream=Fyml, sort_keys=False)
|
|
|
|
print(f"{GRE} Pin file successfully generated in {YEL}{yml_path.name}{ENC}")
|
|
|
|
except Exception as e:
|
|
print(f"{RED} Error in configuration file generation of {fname} in path {YEL}{output_dir}{ENC}. Details: {e}")
|
|
|
|
|
|
def cellLoad(gdsName,path = "",cellName=None,pinFileName = None,cellresued=None):
|
|
|
|
if (gdsName[-4:] == ".gds"):
|
|
fname = gdsName[0:-4]
|
|
|
|
else:
|
|
fname = gdsName
|
|
gdsName = gdsName + ".gds"
|
|
|
|
if (cellName is None):
|
|
cellName = fname
|
|
|
|
""" Older json pin file recording system """
|
|
if (pinFileName is None):
|
|
if (os.path.isfile(path + fname + ".yml")):
|
|
print(f"{path +fname}.yml file exist")
|
|
pinFileName = fname + ".yml"
|
|
elif (os.path.isfile(path + fname + ".json")):
|
|
print(f"{path +fname}.json file exist")
|
|
pinFileName = fname + ".json"
|
|
else:
|
|
raise Exception(f"No meta para file found for {path + fname}")
|
|
|
|
if (pinFileName.find(".yml") != -1):
|
|
with open(file=path + pinFileName,mode="r") as Fpara:
|
|
paraList = yaml.load(Fpara,Loader=yaml.FullLoader)
|
|
pin_val = paraList["ports"]
|
|
|
|
elif (pinFileName.find(".json") != -1):
|
|
with open(file=path + pinFileName,mode="r") as Fpara:
|
|
pin_val = json.load(Fpara)
|
|
|
|
""" Loading GDS """
|
|
inst = nd.load_gds(filename=path+gdsName,cellname=cellName,newcellname=cellName,cellsreused=cellresued)
|
|
|
|
for keys in pin_val:
|
|
if (keys == "time"):
|
|
continue
|
|
inst._put_pin(name=keys,width=pin_val[keys]["width"],connect=(pin_val[keys]["x"],pin_val[keys]["y"],pin_val[keys]["a"]))
|
|
return inst
|
|
|
|
|
|
def render_gds_to_png(gds_path: Path, png_path: Path):
|
|
"""
|
|
Renders a PNG from a GDS file, handling massive chip dimensions
|
|
by enforcing a strict pixel limit to prevent Cairo memory crashes.
|
|
"""
|
|
try:
|
|
# 1. Read the saved GDS file
|
|
lib = gdstk.read_gds(str(gds_path))
|
|
top_cells = lib.top_level()
|
|
|
|
if not top_cells:
|
|
raise ValueError(f"No top-level cell found in {gds_path.name}")
|
|
|
|
top_cell = top_cells[0]
|
|
|
|
# 2. Define a temporary SVG path
|
|
svg_path = gds_path.with_suffix(".svg")
|
|
|
|
# 3. Export to SVG
|
|
# CRITICAL FIX: Add scaling=1.0. gdstk defaults to 10, which artificially
|
|
# inflates the base SVG document size before Cairo even touches it.
|
|
top_cell.write_svg(str(svg_path), background="white", scaling=1.0)
|
|
|
|
# 4. Convert the SVG to a high-res PNG
|
|
# CRITICAL FIX: Replace `scale=2.0` with `output_width=2048`.
|
|
# Cairo will now force the width to 2048px and auto-calculate the height,
|
|
# ensuring the image is always high-res but never exceeds memory limits.
|
|
cairosvg.svg2png(
|
|
url=str(svg_path),
|
|
write_to=str(png_path),
|
|
output_width=2048 # You can adjust this for more/less resolution
|
|
)
|
|
|
|
# 5. Clean up the temporary SVG file
|
|
if svg_path.exists():
|
|
svg_path.unlink()
|
|
|
|
print(f"{GRE} Image successfully generated: {YEL}{png_path.name}{ENC}")
|
|
|
|
except Exception as e:
|
|
print(f"{RED} Failed to generate PNG for {gds_path.name}: {e}{ENC}")
|
|
|
|
def render_gds_to_svg(gds_path: Path, svg_path: Path):
|
|
"""
|
|
Renders an SVG from a GDS file.
|
|
SVGs are vector-based, allowing infinite zoom for inspecting fine PIC details
|
|
and completely avoiding memory limits associated with pixel-based PNGs.
|
|
"""
|
|
try:
|
|
# 1. Read the saved GDS file
|
|
lib = gdstk.read_gds(str(gds_path))
|
|
top_cells = lib.top_level()
|
|
|
|
if not top_cells:
|
|
raise ValueError(f"No top-level cell found in {gds_path.name}")
|
|
|
|
top_cell = top_cells[0]
|
|
|
|
# 2. Export directly to SVG
|
|
# We remove scaling=1.0 because web browsers handle standard SVG coordinate
|
|
# scaling natively without any memory penalty.
|
|
top_cell.write_svg(str(svg_path), background="white")
|
|
|
|
print(f"{GRE} Image successfully generated: {YEL}{svg_path.name}{ENC}")
|
|
|
|
except Exception as e:
|
|
print(f"{RED} Failed to generate SVG for {gds_path.name}: {e}{ENC}")
|
|
|
|
def BB_gds_gen(gds_name, pin_info,fpath = None, BB_Layer=1200, pin_rec_layer=1205,pin_text_layer=1001):
|
|
# 1. Load the existing GDS file
|
|
library = gdstk.read_gds(gds_name)
|
|
top_cell = library.top_level()[0]
|
|
|
|
# 2. Calculate the Bounding Box (Extents)
|
|
# Returns [(min_x, min_y), (max_x, max_y)]
|
|
bbox = top_cell.bounding_box()
|
|
(min_x, min_y), (max_x, max_y) = bbox
|
|
|
|
# Create the new black box cell
|
|
bb_cell = gdstk.Cell(f"{top_cell.name}_BB")
|
|
output_filename = f"{top_cell.name}_BB"
|
|
|
|
# 3. Create the boundary box on Layer 1001
|
|
# We create a rectangle using the min/max coordinates
|
|
rect = gdstk.rectangle((min_x, min_y), (max_x, max_y), layer=BB_Layer, datatype=0)
|
|
bb_cell.add(rect)
|
|
|
|
# 3.1 Return of the box size
|
|
box_sz = [abs(max_x-min_x),abs(max_y-min_y)]
|
|
|
|
|
|
# 5. Place Pins on Layer 1015
|
|
# Assuming YAML structure: pins: [{name: "P1", x: 10, y: 20, a: 0, width: 2}, ...]
|
|
for pin in pin_info:
|
|
# Rounding to your preferred 0.001 resolution
|
|
px = round(pin_info[pin]['x'], 3)
|
|
py = round(pin_info[pin]['y'], 3)
|
|
pa = pin_info[pin]['a']
|
|
pw = pin_info[pin]['width']
|
|
|
|
if (pw == "None"):
|
|
pw = 0.5
|
|
|
|
# Create a small square box centered at the pin location
|
|
pin_box = gdstk.rectangle(
|
|
(px - pw/2, py - pw/2),
|
|
(px + pw/2, py + pw/2),
|
|
layer=pin_rec_layer
|
|
)
|
|
# Rotate the pin box if necessary
|
|
pin_box.rotate(pa * (3.14159 / 180), center=(px, py))
|
|
bb_cell.add(pin_box)
|
|
|
|
# Add Label/Text for the pin
|
|
text = gdstk.Label(
|
|
text = pin,
|
|
origin = (px, py),
|
|
magnification=3, # Scale text relative to pin width
|
|
layer=pin_text_layer
|
|
)
|
|
bb_cell.add(text)
|
|
|
|
# 6. Save the new Library
|
|
new_lib = gdstk.Library()
|
|
new_lib.add(bb_cell)
|
|
if (fpath[-1] != "\\"):
|
|
fpath = fpath + "\\"
|
|
|
|
new_lib.write_gds(fpath + output_filename+".gds")
|
|
|
|
return box_sz
|
|
|
|
|
|
def generate_bb_and_svgs(gds_name, pin_info, fpath=None, BB_Layer=1200, pin_rec_layer=1205, pin_text_layer=1001, layer_exclude: list = None):
|
|
"""
|
|
Reads an original GDS, generates a Black Box GDS, and outputs two SVGs.
|
|
layer_exclude can contain integers (e.g., 5) to exclude all datatypes on that layer,
|
|
or tuples (e.g., (23, 0)) to exclude specific layer/datatype pairs.
|
|
"""
|
|
if layer_exclude is None:
|
|
layer_exclude = []
|
|
|
|
try:
|
|
gds_path = Path(gds_name)
|
|
output_dir = Path(fpath) if fpath else gds_path.parent
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
base_name = gds_path.stem
|
|
|
|
bb_gds_path = output_dir / f"{base_name}_BB.gds"
|
|
orig_svg_path = output_dir / f"{base_name}.svg"
|
|
bb_svg_path = output_dir / f"{base_name}_BB.svg"
|
|
|
|
library = gdstk.read_gds(str(gds_path))
|
|
|
|
# ---------------------------------------------------------
|
|
# NEW: Smart Exclusion Logic for (Layer, Datatype)
|
|
# ---------------------------------------------------------
|
|
def is_excluded(layer, datatype):
|
|
for item in layer_exclude:
|
|
# Check if the user passed a (layer, datatype) tuple
|
|
if isinstance(item, (tuple, list)) and len(item) == 2:
|
|
if layer == item[0] and datatype == item[1]:
|
|
return True
|
|
# Check if the user passed just a base layer integer
|
|
elif layer == item:
|
|
return True
|
|
return False
|
|
|
|
if layer_exclude:
|
|
for cell in library.cells:
|
|
elements_to_remove = []
|
|
|
|
# Check Polygons (.datatype)
|
|
for p in cell.polygons:
|
|
if is_excluded(p.layer, p.datatype):
|
|
elements_to_remove.append(p)
|
|
|
|
# Check Paths (paths can have multiple parallel traces, so we check all pairs)
|
|
for p in cell.paths:
|
|
if any(is_excluded(l, d) for l, d in zip(p.layers, p.datatypes)):
|
|
elements_to_remove.append(p)
|
|
|
|
# Check Labels (.texttype is the GDS equivalent of datatype for text)
|
|
for lbl in cell.labels:
|
|
if is_excluded(lbl.layer, lbl.texttype):
|
|
elements_to_remove.append(lbl)
|
|
|
|
if elements_to_remove:
|
|
cell.remove(*elements_to_remove)
|
|
# ---------------------------------------------------------
|
|
|
|
top_cells = library.top_level()
|
|
|
|
if not top_cells:
|
|
raise ValueError(f"No top-level cell found in {gds_path.name}")
|
|
top_cell = top_cells[0]
|
|
|
|
bbox = top_cell.bounding_box()
|
|
|
|
if bbox is None:
|
|
min_x, min_y, max_x, max_y = 0.0, 0.0, 0.0, 0.0
|
|
else:
|
|
(min_x, min_y), (max_x, max_y) = bbox
|
|
|
|
box_sz = [abs(max_x - min_x), abs(max_y - min_y)]
|
|
|
|
bb_cell = gdstk.Cell(top_cell.name)
|
|
|
|
rect = gdstk.rectangle((min_x, min_y), (max_x, max_y), layer=BB_Layer, datatype=0)
|
|
bb_cell.add(rect)
|
|
|
|
for pin_name, pin_data in pin_info.items():
|
|
px = round(pin_data['x'], 3)
|
|
py = round(pin_data['y'], 3)
|
|
pa = pin_data['a']
|
|
pw = pin_data.get('width', 0.5)
|
|
|
|
if pw == "None" or pw is None:
|
|
pw = 0.5
|
|
|
|
box_min = (px - pw/2, py - pw/2)
|
|
box_max = (px + pw/2, py + pw/2)
|
|
|
|
orig_pin_box = gdstk.rectangle(box_min, box_max, layer=pin_rec_layer)
|
|
orig_pin_box.rotate(pa * (math.pi / 180), center=(px, py))
|
|
orig_text = gdstk.Label(
|
|
text = pin_name, origin = (px, py), magnification=1, layer=pin_text_layer
|
|
)
|
|
top_cell.add(orig_pin_box)
|
|
top_cell.add(orig_text)
|
|
|
|
bb_pin_box = gdstk.rectangle(box_min, box_max, layer=pin_rec_layer)
|
|
bb_pin_box.rotate(pa * (math.pi / 180), center=(px, py))
|
|
bb_text = gdstk.Label(
|
|
text = pin_name, origin = (px, py), magnification=1, layer=pin_text_layer
|
|
)
|
|
bb_cell.add(bb_pin_box)
|
|
bb_cell.add(bb_text)
|
|
|
|
new_lib = gdstk.Library()
|
|
new_lib.add(bb_cell)
|
|
new_lib.write_gds(str(bb_gds_path))
|
|
print(f"Black Box GDS saved: {bb_gds_path.name}")
|
|
|
|
top_cell.write_svg(str(orig_svg_path), background="white")
|
|
print(f"Original SVG generated: {orig_svg_path.name}")
|
|
|
|
bb_cell.write_svg(str(bb_svg_path), background="white")
|
|
print(f"Black Box SVG generated: {bb_svg_path.name}")
|
|
|
|
return box_sz
|
|
|
|
except Exception as e:
|
|
print(f"Error generating BB and SVGs for {gds_name}: {e}")
|
|
return [0.0, 0.0] |