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]