diff --git a/klayout_package/python/kqcircuits/defaults.py b/klayout_package/python/kqcircuits/defaults.py index 3f6e64c42..831f9ade3 100644 --- a/klayout_package/python/kqcircuits/defaults.py +++ b/klayout_package/python/kqcircuits/defaults.py @@ -106,11 +106,24 @@ default_bar_format = '{l_bar}{bar}| {n_fmt}/{total_fmt} [Elapsed: {elapsed}, Left (eta): {remaining}, {rate_inv_fmt}' \ '{postfix}]' -# dictionary of probepoint types for probepoint export -# key is the refpoint name prefix for a type of probepoints, value is a text representation of the probepoint type -default_probe_types = {"testarray": "testarrays", "qb": "qubits"} -# tuple of refpoint name suffixes that are used (together with probe_types) to identify refpoints as probepoints -default_probe_suffixes = ("_c", "_l") +# refpoint is extracted as probepoint if it contains some string from default_probe_types and +# ends with some substring from default_probe_suffixes + +default_probe_types = ["testarray", "qb"] +default_probe_suffixes = [ + "_l", "_r", "_top", # Test array probepoints + # Single island qubit probepoints + "_probe_ground", "_probe_island", + # Double island qubit probepoints + "_probe_island_1", "_probe_island_2" +] +# for probepoint pair, one is assigned to west and other to east prober based on their x coordinate +# but we also want to set a standard where ground is consistently probed from one side and island +# from the other. This dict defines such rules +recommended_probe_suffix_mapping = { + "_probe_ground": "east", + "_probe_island": "west" +} # Library names in dependency order. Every library should have its dependencies before its own entry. kqc_library_names = ( diff --git a/klayout_package/python/kqcircuits/qubits/double_pads.py b/klayout_package/python/kqcircuits/qubits/double_pads.py index c9aede02f..0b214bcc9 100644 --- a/klayout_package/python/kqcircuits/qubits/double_pads.py +++ b/klayout_package/python/kqcircuits/qubits/double_pads.py @@ -126,6 +126,13 @@ def build(self): self.add_port("drive", pya.DPoint(float(self.drive_position[0]), float(self.drive_position[1])), direction=pya.DVector(float(self.drive_position[0]), float(self.drive_position[1]))) + # Probepoints + self.refpoints["probe_island_1"] = pya.DPoint(0, + self.squid_offset + squid_height / 2 + taper_height + float(self.island1_extent[1]) / 2) + self.refpoints["probe_island_2"] = pya.DPoint(0, + self.squid_offset - squid_height / 2 - taper_height - float(self.island2_extent[1]) / 2) + + def _build_coupler(self, first_island_top_edge): coupler_top_edge = first_island_top_edge + self.coupler_offset + float(self.coupler_extent[1]) coupler_polygon = pya.DPolygon([ diff --git a/klayout_package/python/kqcircuits/qubits/qubit.py b/klayout_package/python/kqcircuits/qubits/qubit.py index 5ac0084ec..4bf50170a 100644 --- a/klayout_package/python/kqcircuits/qubits/qubit.py +++ b/klayout_package/python/kqcircuits/qubits/qubit.py @@ -39,6 +39,19 @@ class Qubit(Element): * possible fluxlines * e-beam layers for SQUIDs * SQUID name parameter + + It is customary to also define probepoints for a qubit. Simply define two refpoints as appropriate probepoints. + For single island qubits:: + + self.refpoints["probe_ground"] = pya.DPoint(...) + self.refpoints["probe_island"] = pya.DPoint(...) + self.cell.shapes(self.get_layer("ground_grid_avoidance")).insert( + pya.DBox(-20.0, -20.0, 20.0, 20.0).moved(self.refpoints["probe_ground"])) + + For double island qubits:: + + self.refpoints["probe_island_1"] = pya.DPoint(...) + self.refpoints["probe_island_2"] = pya.DPoint(...) """ LIBRARY_NAME = "Qubit Library" diff --git a/klayout_package/python/kqcircuits/util/export_helper.py b/klayout_package/python/kqcircuits/util/export_helper.py index 90f2f29b9..7653052c3 100644 --- a/klayout_package/python/kqcircuits/util/export_helper.py +++ b/klayout_package/python/kqcircuits/util/export_helper.py @@ -24,95 +24,290 @@ import argparse from sys import argv from pathlib import Path +from typing import Dict, List, Optional, Tuple, Union from kqcircuits.elements.element import get_refpoints from kqcircuits.defaults import default_layers, TMP_PATH, STARTUPINFO, default_probe_types, default_probe_suffixes, \ - VERSION_PATHS, default_drc_runset, DRC_PATH + recommended_probe_suffix_mapping, VERSION_PATHS, default_drc_runset, DRC_PATH from kqcircuits.klayout_view import KLayoutView, MissingUILibraryException from kqcircuits.pya_resolver import pya, is_standalone_session, klayout_executable_command -def generate_probepoints_json(cell, face='1t1'): - # make autoprober json string for cell with reference points with magical names - if cell is None or face not in ['1t1', '2b1']: - error_text = f"Invalid face '{face}' or 'nil' cell ." - error = ValueError(error_text) - logging.exception(error_text, exc_info=error) - raise error +def _probe_point_coordinate(pos, eu=1e-3, sd=4): + return {"x": round(pos.x * eu, sd), "y": round(pos.y * eu, sd)} + +def _probe_point_to_dpoint(pos, eu=1e-3): + # this doesn't need to be super precise since we currently only use this to compare distances + return pya.DPoint(pos["x"] / eu, pos["y"] / eu) + +# pylint: disable=dangerous-default-value +def generate_probepoints_json(cell: pya.Cell, + face: str = '1t1', + flip_face: Optional[bool] = None, + references: List[str] = ['nw'], + contact: Optional[Union[ + Tuple[pya.DPoint, pya.DPoint], + List[Tuple[pya.DPoint, pya.DPoint]]]] = None) -> Dict: + """For given cell, collects probepoints from cell's refpoints into a json Dict. + + A refpoint is a probepoint if it + + * contains some string from ``default_probe_types`` and + * has a suffix from ``default_probe_suffixes`` + + Json format consists of {'x': x, 'y': y} type 2d points, in millimeter units. + The returned json object consists of: + + * an 'alignment' point, which tells the position of the reference marker defined in references marker and + * 'sites' list. Each entry of the list has a 'west' and 'east' point, and also a unique 'id' as string + + Args: + * cell: cell from which to collect probepoints + * face: name of the face from where to collect probepoints + * flip_face: explicitly specifies if the points should be flipped around the y-axis. + Can be set to None, in which case will infer whether to flip points from the ``face`` arg + * references: a list of markers to use as alignment references. String values are one of + "nw", "ne", "sw", "se". If multiple values supplied, the resulting json will have + "groups" key on top level, with each group containing the marker string as 'id' + and its own 'alignment' and 'sites' values, grouping each site to its closest marker. + * contact: a manually defined contact probe, a tuple of two DPoints. + Can be None so no "contact" site is added, or can be a list if a different "contact" + site is needed for each reference + """ + validations = [ + (cell is None, "Cell is null"), + (not references, "Can't use empty list of references"), + (isinstance(contact, tuple) and len(contact) < 2, "Singular contact must be tuple of two DPoints"), + (isinstance(contact, list) and (len(contact) != len(references) or any(len(c) < 2 for c in contact)), + "List of contacts should define a tuple of two DPoints for each reference") + ] + for check, error_text in validations: + if check: + error = ValueError(error_text) + logging.exception(error_text, exc_info=error) + raise error layout = cell.layout() refpoints = get_refpoints(layout.layer(default_layers["refpoints"]), cell) - # Check existence of standard markers important for us - if f"{face}_marker_nw" in refpoints and f"{face}_marker_se" in refpoints: - markers = {'NW': refpoints[f"{face}_marker_nw"], 'SE': refpoints[f"{face}_marker_se"]} + # check existence of reference markers + markers = {} + for reference in references: + marker_refpoint = f"{face}_marker_{reference.lower()}" + if marker_refpoint not in refpoints: + to_legacy_face_name = {face: '', '1t1': 'b', '2b1': 't'} + legacy_marker_refpoint = f"{to_legacy_face_name[face]}_marker_{reference.lower()}" + if legacy_marker_refpoint in refpoints: + marker_refpoint = legacy_marker_refpoint + else: + logging.warning((f"The marker or at least its refpoint {marker_refpoint} " + f"is missing in the cell {cell.name}!")) + if pya.DPoint(1500, 8500) not in markers.values(): + logging.warning(f"Setting marker {marker_refpoint} to DPoint(1500, 8500)") + markers[reference.upper()] = pya.DPoint(1500, 8500) + continue + markers[reference.upper()] = refpoints[marker_refpoint] + + # if not explicitely stated to flip the face, deduce from face string + if flip_face is None: + if len(face) < 2: # legacy face name + flip_face = face == "t" + else: # current face name + flip_face = face[1] == "b" + # get boundaries of the chip dimensions + matching_layer = [l for l in layout.layer_infos() + if l.name in [f"{face}_base_metal_gap_wo_grid", f"{face}*base*metal*gap*wo*grid"]] + if matching_layer: + bbox_for_face = cell.dbbox_per_layer(layout.layer(matching_layer[0])) else: - logging.error(f"There are no usable markers in {face}-face of the cell! Not a Chip?") - return {} - + logging.warning(f"No geometry found at layer {face}_base_metal_gap_wo_grid!") + bbox_for_face = pya.DBox(1500, 1500, 8500, 8500) if flip_face else pya.DBox(0, 0, 10000, 10000) + logging.warning(f"Assuming chip dimensions are at {bbox_for_face}") + # define transformation function to apply to each DPoint object + transform = lambda point: pya.DPoint(point - bbox_for_face.p1) # flip top-markers back to top side - if face == '2b1': - origin = refpoints["1t1_marker_se"] - markers = {k: flip(v, origin) for k, v in markers.items()} - - eu = 1e-3 # export unit + if flip_face: + flip_origin = pya.DPoint(bbox_for_face.p2.x, bbox_for_face.p1.y) + transform = lambda point: pya.DPoint(flip_origin.x - point.x, point.y - flip_origin.y) + markers = {k: transform(v) for k, v in markers.items()} # initialize dictionaries for each probe point group groups = {} - for probe_name in default_probe_types.values(): - for marker_name, marker in markers.items(): - groups[f"{probe_name} {marker_name}"] = { - "alignment": {"x": round(marker.x * eu, 3), "y": round(marker.y * eu, 3)}, - "pads": [] - } - - # divide probe points into groups by closest marker + for marker_name, marker in markers.items(): + groups[marker_name] = { + "alignment": _probe_point_coordinate(marker), + "sites": [] + } + + # first collect sites before grouping them + sites = [] for probepoint_name, probepoint in refpoints.items(): + probepoint = transform(probepoint) name_type = probepoint_name.split("_")[0] # if name_type starts with some probe_type, truncate name_type to be the probe_type for probe_type in default_probe_types: - if name_type.startswith(probe_type): - name_type = probe_type + if name_type.lower().startswith(probe_type.lower()): + name_type = probe_type.lower() break - # does the name correspond to a probepoint? - if name_type in default_probe_types.keys() and probepoint_name.endswith(default_probe_suffixes): - - if face == '2b1': - probepoint = flip(probepoint, origin) - - best_distance = 1e99 - closest_marker = None - for marker, refpoint in markers.items(): - if refpoint.distance(probepoint) < best_distance: - best_distance = refpoint.distance(probepoint) - closest_marker = marker - - groups[f"{default_probe_types[name_type]} {closest_marker}"]["pads"].append({ - "id": probepoint_name, - "x": round(probepoint.x * eu, 3), - "y": round(probepoint.y * eu, 3), + # extract suffix if probepoint_name uses one from default_probe_suffixes + suffixes = [s for s in default_probe_suffixes if probepoint_name.endswith(s)] + if name_type in default_probe_types and suffixes: + remove_suffix_tokens = max(len(suffixes[0].split('_')) - 2, 1) + probe_name = '_'.join(probepoint_name.split('_')[:-remove_suffix_tokens]) + # find site with id value as this probepoint prefix + probepoint_sites = [s for s in sites if s["id"] == probe_name] + probepoint_entry = _probe_point_coordinate(probepoint) + # create a new site if none found + if not probepoint_sites: + sites.append({"id": probe_name, "west": probepoint_entry}) + else: + site = probepoint_sites[0] + if "east" not in site: + if probepoint_entry["x"] < site["west"]["x"]: + site["east"] = site["west"] + direction = "west" + else: + direction = "east" + site[direction] = probepoint_entry + expected_direction = recommended_probe_suffix_mapping.get(suffixes[0]) + if expected_direction is not None and expected_direction != direction: + logging.warning((f"Probepoint {probepoint_name} was mapped to {direction}, " + f"but recommended direction for {suffixes[0]} is {expected_direction}")) + else: + # limited support for more than two point probing + for key in site: + if key == "id": + continue + sites.append({ + "east": site[key] if site[key]["x"] > probepoint_entry["x"] else probepoint_entry, + "id": f"{probe_name}{suffixes[0]}_{key}", + "west": probepoint_entry if site[key]["x"] > probepoint_entry["x"] else site[key], + }) + + # sanity check that each site has exactly east and west probe + for site in sites: + if set(site.keys()) != {"west", "east", "id"}: + logging.warning(f"Malformed site object detected: {site}") + if "east" in site and "west" not in site: + site["west"] = site["east"] + elif "west" in site and "east" not in site: + site["east"] = site["west"] + elif "east" not in site and "west" not in site: + site["east"] = {"x": 0.0, "y": 0.0} + site["west"] = {"x": 0.0, "y": 0.0} + + # reason for sorting is to make the exported json more deterministic + sites.sort(key=lambda site: site["id"]) + for idx,_ in enumerate(sites): + sites[idx] = dict(sorted(sites[idx].items())) + + # divide probe points into groups by closest marker (multireference only) + for site in sites: + midpoint = {"x": (site["west"]["x"] + site["east"]["x"]) / 2., + "y": (site["west"]["y"] + site["east"]["y"]) / 2.} + midpoint = _probe_point_to_dpoint(midpoint) + _, closest_marker = sorted( + [(refpoint.distance(midpoint), marker) for marker, refpoint in markers.items()], + key=lambda x: x[0])[0] # sort by distance, get closest tuple, get marker + groups[closest_marker]["sites"].append(site) + + # add manual "contact" entries + if contact is not None: + if isinstance(contact, tuple): + contact = [contact] * len(references) + for idx, group in enumerate(groups.values()): + contact1, contact2 = contact[idx] + contact1 = _probe_point_coordinate(transform(contact1)) + contact2 = _probe_point_coordinate(transform(contact2)) + west_is_1 = contact1["x"] < contact2["x"] + group["sites"].append({ + "east": contact2 if west_is_1 else contact1, + "id": "contact", + "west": contact1 if west_is_1 else contact2, }) - # remove empty groups - groups = {k: v for k, v in groups.items() if v["pads"]} + # find probepoint duplicates (within tolerance) and only keep the one with longer id name + for group_key, group in groups.items(): + for i, site1 in enumerate(group["sites"]): + if site1 == {}: + continue + for j, site2 in enumerate(group["sites"][i+1:]): + if site2 == {}: + continue + too_close = True + for side in ["west", "east"]: + x1 = site1[side]["x"] + x2 = site2[side]["x"] + y1 = site1[side]["y"] + y2 = site2[side]["y"] + if ((x1 - x2) ** 2) + ((y1 - y2) ** 2) > (0.001) ** 2: + too_close = False + if too_close: + logging.warning( + f"Found two sites '{site1['id']}' and '{site2['id']}' with similar coordinates (respectively)") + logging.warning( + f" west {site1['west']['x']},{site1['west']['y']} = {site2['west']['x']},{site2['west']['y']}") + logging.warning( + f" east {site1['east']['x']},{site1['east']['y']} = {site2['east']['x']},{site2['east']['y']}") + logging.warning((" will only keep the site " + f"'{site1['id'] if len(site1['id']) > len(site2['id']) else site2['id']}'")) + group["sites"][i+j+1 if len(site1["id"]) > len(site2["id"]) else i].clear() + if site1 == {}: + break + # pylint: disable=unnecessary-dict-index-lookup + groups[group_key]["sites"] = [site for site in group["sites"] if site != {}] - # sort from left to right, bottom to top, for faster probing - for group in groups.values(): - group["pads"] = sorted(group["pads"], key=lambda k: (k['x'], k['y'])) - - # define JSON format - comp_dict = { - "groups": [{"id": name, **group} for name, group in groups.items()] - } - - comp_json = json.dumps(comp_dict, indent=2, sort_keys=True) - - return comp_json + # remove empty groups + groups = {k: v for k, v in groups.items() if v["sites"]} + + # leave out groups key if only one group + if len(groups) == 1: + return list(groups.values())[0] + return {"groups": [{"id": name, **group} for name, group in groups.items()]} + + +def generate_probepoints_from_file(cell_file: str, + face: str = '1t1', + flip_face: Optional[bool] = None, + references: List[str] = ['nw'], + contact: Optional[Union[ + Tuple[pya.DPoint, pya.DPoint], + List[Tuple[pya.DPoint, pya.DPoint]]]] = None) -> Dict: + """For an OAS and GDS file containing a chip at its top cell, + collects probepoints from cell's refpoints into a json Dict. + A refpoint is a probepoint if it + + * contains some string from ``default_probe_types`` and + * has a suffix from ``default_probe_suffixes`` + + Json format consists of {'x': x, 'y': y} type 2d points, in millimeter units. + The returned json object consists of: + + * an 'alignment' point, which tells the position of the reference marker defined in references marker and + * 'sites' list. Each entry of the list has a 'west' and 'east' point, and also a unique 'id' as string + + Args: + * cell_file: file path to the OAS or GDS file containing a chip at its top cell + * face: name of the face from where to collect probepoints + * flip_face: explicitly specifies if the points should be flipped around the y-axis. + Can be set to None, in which case will infer whether to flip points from the ``face`` arg + * references: a list of markers to use as alignment references. String values are one of + "nw", "ne", "sw", "se". If multiple values supplied, the resulting json will have + "groups" key on top level, with each group containing the marker string as 'id' + and its own 'alignment' and 'sites' values, grouping each site to its closest marker. + * contact: a manually defined contact probe, a tuple of two DPoints. + Can be None so no "contact" site is added, or can be a list if a different "contact" + site is needed for each reference + """ + load_opts = pya.LoadLayoutOptions() + load_opts.cell_conflict_resolution = pya.LoadLayoutOptions.CellConflictResolution.RenameCell + view = KLayoutView() + layout = view.layout + layout.read(cell_file, load_opts) + cell = layout.top_cells()[-1] + return generate_probepoints_json(cell, face, flip_face, references, contact) -def flip(point, origin=pya.DPoint(0,0)): - """Gets correct flip chip coordinates by setting a new origin and mirroring ``point`` by the y-axis.""" - return pya.DPoint(origin.x - point.x, point.y - origin.y) def create_or_empty_tmp_directory(dir_name): """Creates directory into TMP_PATH or removes its content if it exists. diff --git a/klayout_package/python/scripts/macros/export/export_probepoints_json.lym b/klayout_package/python/scripts/macros/export/export_probepoints_json.lym index 1f5a04109..f68d17869 100644 --- a/klayout_package/python/scripts/macros/export/export_probepoints_json.lym +++ b/klayout_package/python/scripts/macros/export/export_probepoints_json.lym @@ -8,6 +8,7 @@ false false + 0 false @@ -34,13 +35,26 @@ """Prints the probepoints of a chip as JSON. Assumes that a single chip exists in the top cell.""" +import json from kqcircuits.klayout_view import KLayoutView from kqcircuits.util.export_helper import generate_probepoints_json top_cell = KLayoutView(current=True).active_cell +### Set file location, or leave empty to get probepoint JSON empty +FILE_LOCATION = "" ### Action -# Note that this works on the bottom face by default. For the top, specify a second 't' argument. -print(generate_probepoints_json(top_cell)) +# See API docs on how to best call generate_probepoints_json for your purposes +probepoint_json = generate_probepoints_json(top_cell, + face='1t1', + references=['nw'], + contact=None) +json_str = json.dumps(probepoint_json, indent=2) + +if FILE_LOCATION: + with open(FILE_LOCATION, 'w') as file: + file.write(json_str) +else: + print(json_str) diff --git a/klayout_package/python/scripts/macros/generate/visualise_probepoints.lym b/klayout_package/python/scripts/macros/generate/visualise_probepoints.lym new file mode 100644 index 000000000..304118214 --- /dev/null +++ b/klayout_package/python/scripts/macros/generate/visualise_probepoints.lym @@ -0,0 +1,93 @@ + + + Visualise probepoints from json file to layout + + pymacros + + + + false + false + 0 + + false + + + python + + # This code is part of KQCircuits +# Copyright (C) 2023 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements +# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). + + +"""Visualises the probepoints in external JSON file. +Open or generate chip geometry then run this macro with specified filename +to compare that the probepoints align with chip components. +After running this macro, right click the Layers panel (on the right by default) +and choose "Add Other Layer Entries". +""" + +import json +from kqcircuits.klayout_view import KLayoutView +from kqcircuits.pya_resolver import pya +from kqcircuits.util.export_helper import generate_probepoints_json + +# SPECIFY ARGUMENTS +FILENAME = "" # Probepoint JSON file to visualise +FLIP_FACE = False # Mirrors points wrt Y-axis. Recommended for flipchips +FACE = '1t1' # Specify substrate face to probe + +cell = KLayoutView(current=True).active_cell + +with open(FILENAME) as f: + probepoint_json = json.load(f) + +bbox_for_face = cell.dbbox_per_layer( + cell.layout().layer( + [l for l in cell.layout().layer_infos() if l.name == f"{FACE}_base_metal_gap_wo_grid"][0] + ) +) + +def to_dtext(text_string, json_object): + x = json_object["x"] * 1e3 + y = json_object["y"] * 1e3 + if FLIP_FACE: + x = bbox_for_face.p2.x - x + else: + x += bbox_for_face.p1.x + y += bbox_for_face.p1.y + return pya.DText(text_string, x, y) + +def visualise_point(group, layer_string, text_string, json_object): + layer_name = layer_string if not group else f"{group}_{layer_string}" + cell.shapes(cell.layout().layer(layer_name)).insert(to_dtext(text_string, json_object)) + +def visualise_group(group): + visualise_point(group.get("id"), "alignment", "alignment", probepoint_json["alignment"]) + for site in probepoint_json["sites"]: + visualise_point(group.get("id"), "east", site["id"], site["east"]) + visualise_point(group.get("id"), "west", site["id"], site["west"]) + +for layer in cell.layout().layer_infos(): + if "alignment" in layer.name or "west" in layer.name or "east" in layer.name: + cell.shapes(cell.layout().layer(layer)).clear() + +if "groups" in probepoint_json: + for group in groups: + visualise_group(group) +else: + visualise_group(probepoint_json) + diff --git a/tests/util/export_helper/test_generate_probepoints_json.py b/tests/util/export_helper/test_generate_probepoints_json.py new file mode 100644 index 000000000..8ff7c20ee --- /dev/null +++ b/tests/util/export_helper/test_generate_probepoints_json.py @@ -0,0 +1,615 @@ +# This code is part of KQCircuits +# Copyright (C) 2023 IQM Finland Oy +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public +# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with this program. If not, see +# https://www.gnu.org/licenses/gpl-3.0.html. +# +# The software distribution should follow IQM trademark policy for open-source software +# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements +# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization). + +import pytest +from kqcircuits.defaults import default_layers +from kqcircuits.chips.chip import Chip +from kqcircuits.elements.element import insert_cell_into +from kqcircuits.pya_resolver import pya +from kqcircuits.util.export_helper import generate_probepoints_json + +def add_refpoint(cell, name, x, y): + refpoint_layer = cell.layout().layer(default_layers["refpoints"]) + cell.shapes(refpoint_layer).insert(pya.DText(name, x, y)) + +def site_is_at(group, site_id, west_x, west_y, east_x, east_y): + site = [s for s in group["sites"] if s["id"] == site_id][0] + assert site, f"Site with id {site_id} was not exported: {group}" + expected_value = {"east": {"x": east_x, "y": east_y}, + "id": site_id, + "west": {"x": west_x, "y": west_y}} + assert site == expected_value, f"Expected {expected_value}, got {site}" + +@pytest.fixture +def layout(): + return pya.Layout() + +@pytest.fixture +def empty_cell(layout): + """Adds two substrate flipchip + + with dimensions (0, 0, 10000, 1000) and (1500, 1500, 8500, 8500) + with bottom chip markers at (1500, 1500, 8500, 8500) + and top markers at (7500, 7500, 7500, 7500) relative to bottom chip origin, + or (1000, 1000, 6000, 6000) relative to top chip origin and oritentation + """ + cell = layout.create_cell("test") + insert_cell_into(cell, Chip, frames_enabled=[0,1]) + return cell + +@pytest.fixture +def dummy_cell(empty_cell): + """Add some refpoints so that probepoint generator returns something.""" + add_refpoint(empty_cell, "testarray_1_probe_0_l", 4400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 4000, 8600) + return empty_cell + +@pytest.fixture +def legacy_cell(dummy_cell): + """Recreates empty_cell such that layer and refpoint names uses legacy face naming scheme.""" + bottom_bbox = dummy_cell.dbbox_per_layer( + dummy_cell.layout().layer( + [l for l in dummy_cell.layout().layer_infos() if l.name == "1t1_base_metal_gap_wo_grid"][0] + ) + ) + top_bbox = dummy_cell.dbbox_per_layer( + dummy_cell.layout().layer( + [l for l in dummy_cell.layout().layer_infos() if l.name == "2b1_base_metal_gap_wo_grid"][0] + ) + ) + dummy_cell.shapes(dummy_cell.layout().layer(pya.LayerInfo(1000, 0, "b_base_metal_gap_wo_grid"))).insert(bottom_bbox) + dummy_cell.shapes(dummy_cell.layout().layer(pya.LayerInfo(1001, 0, "t_base_metal_gap_wo_grid"))).insert(top_bbox) + add_refpoint(dummy_cell, "b_marker_nw", 1500, 8500) + add_refpoint(dummy_cell, "t_marker_nw", 7500, 7500) + return dummy_cell + +def test_finds_bottom_face_marker(dummy_cell, caplog): + probepoints = generate_probepoints_json(dummy_cell) + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + assert probepoints["alignment"] == {"x": 1.5, "y": 8.5}, "Expected 1t1 face NW marker at 1.5,8.5" + +def test_finds_top_face_marker(dummy_cell, caplog): + probepoints = generate_probepoints_json(dummy_cell, face="2b1") + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + assert probepoints["alignment"] == {"x": 1.0, "y": 6.0}, "Expected 2b1 face NW marker at 1.0,6.0" + +def test_finds_bottom_face_legacy_marker(legacy_cell, caplog): + probepoints = generate_probepoints_json(legacy_cell, face="b") + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + assert probepoints["alignment"] == {"x": 1.5, "y": 8.5}, "Expected b face NW marker at 1.5,8.5" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + +def test_finds_top_face_legacy_marker(legacy_cell, caplog): + probepoints = generate_probepoints_json(legacy_cell, face="t") + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + assert probepoints["alignment"] == {"x": 1.0, "y": 6.0}, "Expected t face NW marker at 1.0,6.0" + top_chip_width, top_chip_offset_x, top_chip_offset_y = 7.0, 1.5, 1.5 + site_is_at(probepoints, "testarray_1_probe_0", + round(top_chip_width - (4.4 - top_chip_offset_x), 4), + round(8.6 - top_chip_offset_y, 4), + round(top_chip_width - (4.0 - top_chip_offset_x), 4), + round(8.6 - top_chip_offset_y, 4)) + +def test_warn_if_geometry_missing1(dummy_cell, caplog): + add_refpoint(dummy_cell, "1x1_marker_nw", 1500, 1500) + generate_probepoints_json(dummy_cell, face="1x1") + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 2, "Expected to have a warning in the log for not finding geometry" + assert "No geometry found at layer 1x1_base_metal_gap_wo_grid!" in log_messages[0],\ + f"Unexpected warning message: {log_messages[0]}" + assert "Assuming chip dimensions are at (0,0;10000,10000)" in log_messages[1],\ + f"Unexpected warning message: {log_messages[1]}" + +def test_warn_if_geometry_missing2(layout, caplog): + cell = layout.create_cell("test") + insert_cell_into(cell, Chip) + add_refpoint(cell, "2b1_marker_nw", 7500, 7500) + generate_probepoints_json(cell, face="2b1") + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 2, "Expected to have a warning in the log for not finding geometry" + assert "No geometry found at layer 2b1_base_metal_gap_wo_grid!" in log_messages[0],\ + f"Unexpected warning message: {log_messages[0]}" + assert "Assuming chip dimensions are at (1500,1500;8500,8500)" in log_messages[1],\ + f"Unexpected warning message: {log_messages[1]}" + +def test_warn_if_missing_marker_refpoint_and_give_default1(layout, caplog): + cell = layout.create_cell("test") + cell.shapes(cell.layout().layer( + pya.LayerInfo(1000, 0, "1t1_base_metal_gap_wo_grid") + )).insert(pya.DBox(0, 0, 10000, 10000)) + add_refpoint(cell, "testarray_1_probe_0_l", 4400, 8600) + add_refpoint(cell, "testarray_1_probe_0_r", 4000, 8600) + probepoints = generate_probepoints_json(cell) + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 2, "Expected to have a warning in the log for not finding a marker refpoint" + assert "The marker or at least its refpoint 1t1_marker_nw is missing in the cell test!" in log_messages[0],\ + f"Unexpected warning message: {log_messages[0]}" + assert "Setting marker 1t1_marker_nw to DPoint(1500, 8500)" in log_messages[1],\ + f"Unexpected warning message: {log_messages[1]}" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + +def test_warn_if_missing_marker_refpoint_and_give_default2(dummy_cell, caplog): + dummy_cell.shapes(dummy_cell.layout().layer( + pya.LayerInfo(1000, 0, "1x1_base_metal_gap_wo_grid") + )).insert(pya.DBox(0, 0, 10000, 10000)) + probepoints = generate_probepoints_json(dummy_cell, face="1x1") + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 2, "Expected to have a warning in the log for not finding a marker refpoint" + assert "The marker or at least its refpoint 1x1_marker_nw is missing in the cell test!" in log_messages[0],\ + f"Unexpected warning message: {log_messages[0]}" + assert "Setting marker 1x1_marker_nw to DPoint(1500, 8500)" in log_messages[1],\ + f"Unexpected warning message: {log_messages[1]}" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + +def test_warn_if_missing_marker_refpoint_and_give_default3(dummy_cell, caplog): + probepoints = generate_probepoints_json(dummy_cell, references=['xw']) + log_messages = [x.message for x in caplog.records] + assert len(log_messages) > 0, "Expected to have a warning in the log for not finding a marker refpoint" + assert "The marker or at least its refpoint 1t1_marker_xw is missing in the cell test!" in log_messages[0],\ + f"Unexpected warning message: {log_messages[0]}" + assert "Setting marker 1t1_marker_xw to DPoint(1500, 8500)" in log_messages[1],\ + f"Unexpected warning message: {log_messages[1]}" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + +def test_warn_if_missing_marker_refpoint_multireference(dummy_cell, caplog): + add_refpoint(dummy_cell, "testarray_2_probe_0_l", 4400, 2600) + add_refpoint(dummy_cell, "testarray_2_probe_0_r", 4000, 2600) + probepoints = generate_probepoints_json(dummy_cell, references=['nw', 'xw', 'se']) + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 1, "Expected to have exactly one warning in the log for not finding a marker refpoint" + assert "The marker or at least its refpoint 1t1_marker_xw is missing in the cell test!" in log_messages[0],\ + f"Unexpected warning message: {log_messages[0]}" + nw_sites = [g for g in probepoints["groups"] if g["id"] == "NW"][0] + se_sites = [g for g in probepoints["groups"] if g["id"] == "SE"][0] + site_is_at(nw_sites, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + site_is_at(se_sites, "testarray_2_probe_0", 4.0, 2.6, 4.4, 2.6) + +def test_fail_if_cell_is_null(): + with pytest.raises(ValueError) as expected_error: + generate_probepoints_json(None) + assert "Cell is null" in str(expected_error.value) + +def test_fail_if_references_is_empty(dummy_cell): + with pytest.raises(ValueError) as expected_error: + generate_probepoints_json(dummy_cell, references=[]) + assert "Can't use empty list of references" in str(expected_error.value) + +def test_fail_if_malformed_contact_arg(dummy_cell): + with pytest.raises(ValueError) as expected_error: + generate_probepoints_json(dummy_cell, contact=(pya.DPoint(5000, 5000),)) + assert "Singular contact must be tuple of two DPoints" in str(expected_error.value) + +def test_fail_if_contact_list_does_not_match_references1(): + with pytest.raises(ValueError) as expected_error: + generate_probepoints_json(dummy_cell, references=['nw', 'sw', 'se', 'ne'], contact=[ + (pya.DPoint(5000, 5000), pya.DPoint(5000, 5100)), + (pya.DPoint(4000, 5000), pya.DPoint(4000, 5100)), + (pya.DPoint(3000, 5000), pya.DPoint(3000, 5100)), + ]) + assert "List of contacts should define a tuple of two DPoints for each reference" in str(expected_error.value) + +def test_fail_if_contact_list_does_not_match_references2(): + with pytest.raises(ValueError) as expected_error: + generate_probepoints_json(dummy_cell, references=['nw', 'sw', 'se', 'ne'], contact=[ + (pya.DPoint(5000, 5000), pya.DPoint(5000, 5100)), + (pya.DPoint(4000, 5000), pya.DPoint(4000, 5100)), + (pya.DPoint(3000, 5000), pya.DPoint(3000, 5100)), + (pya.DPoint(6000, 5000), pya.DPoint(6000, 5100)), + (pya.DPoint(7000, 5000), pya.DPoint(7000, 5100)), + ]) + assert "List of contacts should define a tuple of two DPoints for each reference" in str(expected_error.value) + +def test_fail_if_malformed_contact_arg_in_list(): + with pytest.raises(ValueError) as expected_error: + generate_probepoints_json(dummy_cell, references=['nw', 'sw', 'se', 'ne'], contact=[ + (pya.DPoint(5000, 5000), pya.DPoint(5000, 5100)), + (pya.DPoint(4000, 5000), pya.DPoint(4000, 5100)), + (pya.DPoint(3000, 5000),), + (pya.DPoint(6000, 5000), pya.DPoint(6000, 5100)), + ]) + assert "List of contacts should define a tuple of two DPoints for each reference" in str(expected_error.value) + +def test_generates_probepoints(dummy_cell, caplog): + add_refpoint(dummy_cell, "QB1_probe_island_1", 3400, 2600) + add_refpoint(dummy_cell, "QB1_probe_island_2", 3000, 2600) + add_refpoint(dummy_cell, "QB2_probe_ground", 5400, 5600) + add_refpoint(dummy_cell, "QB2_probe_island", 5000, 5600) + probepoints = generate_probepoints_json(dummy_cell) + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + site_is_at(probepoints, "QB1_probe", 3.0, 2.6, 3.4, 2.6) + site_is_at(probepoints, "QB2_probe", 5.0, 5.6, 5.4, 5.6) + +def test_top_face_probepoints(dummy_cell, caplog): + add_refpoint(dummy_cell, "QB1_probe_island_1", 3000, 2600) + add_refpoint(dummy_cell, "QB1_probe_island_2", 3400, 2600) + add_refpoint(dummy_cell, "QB2_probe_island", 5400, 5600) + add_refpoint(dummy_cell, "QB2_probe_ground", 5000, 5600) + probepoints = generate_probepoints_json(dummy_cell, face="2b1") + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + top_chip_width, top_chip_offset_x, top_chip_offset_y = 7.0, 1.5, 1.5 + site_is_at(probepoints, "testarray_1_probe_0", + round(top_chip_width - (4.4 - top_chip_offset_x), 4), + round(8.6 - top_chip_offset_y, 4), + round(top_chip_width - (4.0 - top_chip_offset_x), 4), + round(8.6 - top_chip_offset_y, 4)) + site_is_at(probepoints, "QB1_probe", + round(top_chip_width - (3.4 - top_chip_offset_x), 4), + round(2.6 - top_chip_offset_y, 4), + round(top_chip_width - (3.0 - top_chip_offset_x), 4), + round(2.6 - top_chip_offset_y, 4)) + site_is_at(probepoints, "QB2_probe", + round(top_chip_width - (5.4 - top_chip_offset_x), 4), + round(5.6 - top_chip_offset_y, 4), + round(top_chip_width - (5.0 - top_chip_offset_x), 4), + round(5.6 - top_chip_offset_y, 4)) + +def test_top_face_probepoints_no_flipping(dummy_cell, caplog): + add_refpoint(dummy_cell, "QB1_probe_island_1", 3400, 2600) + add_refpoint(dummy_cell, "QB1_probe_island_2", 3000, 2600) + add_refpoint(dummy_cell, "QB2_probe_ground", 5400, 5600) + add_refpoint(dummy_cell, "QB2_probe_island", 5000, 5600) + probepoints = generate_probepoints_json(dummy_cell, face="2b1", flip_face=False) + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + top_chip_offset_x, top_chip_offset_y = 1.5, 1.5 + site_is_at(probepoints, "testarray_1_probe_0", + round(4.0 - top_chip_offset_x, 4), + round(8.6 - top_chip_offset_y, 4), + round(4.4 - top_chip_offset_x, 4), + round(8.6 - top_chip_offset_y, 4)) + site_is_at(probepoints, "QB1_probe", + round(3.0 - top_chip_offset_x, 4), + round(2.6 - top_chip_offset_y, 4), + round(3.4 - top_chip_offset_x, 4), + round(2.6 - top_chip_offset_y, 4)) + site_is_at(probepoints, "QB2_probe", + round(5.0 - top_chip_offset_x, 4), + round(5.6 - top_chip_offset_y, 4), + round(5.4 - top_chip_offset_x, 4), + round(5.6 - top_chip_offset_y, 4)) + +def test_warn_if_not_recommended_west_east_assigned_bottom_face(dummy_cell, caplog): + add_refpoint(dummy_cell, "QB2_probe_island", 5400, 5600) + add_refpoint(dummy_cell, "QB2_probe_ground", 5000, 5600) + probepoints = generate_probepoints_json(dummy_cell) + log_messages = [x.message for x in caplog.records] + assert len(caplog.records) == 1,\ + "Expected to have exactly one warning in the log for ground and island probes having non-standard sides" + str1 = "Probepoint QB2_probe_island was mapped to east, but recommended direction for _probe_island is west" + str2 = "Probepoint QB2_probe_ground was mapped to west, but recommended direction for _probe_ground is east" + assert str1 in log_messages[0] or str2 in log_messages[0], f"Unexpected warning message: {log_messages[0]}" + site_is_at(probepoints, "QB2_probe", 5.0, 5.6, 5.4, 5.6) + +def test_warn_if_not_recommended_west_east_assigned_top_face(dummy_cell, caplog): + add_refpoint(dummy_cell, "QB2_probe_ground", 5400, 5600) + add_refpoint(dummy_cell, "QB2_probe_island", 5000, 5600) + probepoints = generate_probepoints_json(dummy_cell, face="2b1") + log_messages = [x.message for x in caplog.records] + assert len(caplog.records) == 1,\ + "Expected to have exactly one warning in the log for ground and island probes having non-standard sides" + str1 = "Probepoint QB2_probe_island was mapped to east, but recommended direction for _probe_island is west" + str2 = "Probepoint QB2_probe_ground was mapped to west, but recommended direction for _probe_ground is east" + assert str1 in log_messages[0] or str2 in log_messages[0], f"Unexpected warning message: {log_messages[0]}" + top_chip_width, top_chip_offset_x, top_chip_offset_y = 7.0, 1.5, 1.5 + site_is_at(probepoints, "QB2_probe", + round(top_chip_width - (5.4 - top_chip_offset_x), 4), + round(5.6 - top_chip_offset_y, 4), + round(top_chip_width - (5.0 - top_chip_offset_x), 4), + round(5.6 - top_chip_offset_y, 4)) + +def test_warn_if_not_recommended_west_east_assigned_top_face_no_flip(dummy_cell, caplog): + add_refpoint(dummy_cell, "QB2_probe_island", 5400, 5600) + add_refpoint(dummy_cell, "QB2_probe_ground", 5000, 5600) + probepoints = generate_probepoints_json(dummy_cell, face="2b1", flip_face=False) + log_messages = [x.message for x in caplog.records] + assert len(caplog.records) == 1,\ + "Expected to have exactly one warning in the log for ground and island probes having non-standard sides" + str1 = "Probepoint QB2_probe_island was mapped to east, but recommended direction for _probe_island is west" + str2 = "Probepoint QB2_probe_ground was mapped to west, but recommended direction for _probe_ground is east" + assert str1 in log_messages[0] or str2 in log_messages[0], f"Unexpected warning message: {log_messages[0]}" + top_chip_offset_x, top_chip_offset_y = 1.5, 1.5 + site_is_at(probepoints, "QB2_probe", + round(5.0 - top_chip_offset_x, 4), + round(5.6 - top_chip_offset_y, 4), + round(5.4 - top_chip_offset_x, 4), + round(5.6 - top_chip_offset_y, 4)) + +def test_probepoints_with_multiple_references1(empty_cell): + add_refpoint(empty_cell, "QB1_probe_island_1", 2400, 5000) + add_refpoint(empty_cell, "QB1_probe_island_2", 2000, 5000) + add_refpoint(empty_cell, "testarray_2_probe_0_l", 8400, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 8000, 2600) + add_refpoint(empty_cell, "testarray_1_probe_0_l", 2400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 2000, 8600) + add_refpoint(empty_cell, "QB2_probe_ground", 8400, 5000) + add_refpoint(empty_cell, "QB2_probe_island", 8000, 5000) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'se']) + assert set(probepoints.keys()) == {"groups"}, "For multireference probepoint json expected to have 'groups' "\ + f"member on top-level, was {probepoints}" + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"] + assert pp_nw, "No probepoint group extracted with id 'NW'" + assert pp_se, "No probepoint group extracted with id 'SE'" + pp_nw, pp_se = pp_nw[0], pp_se[0] + assert pp_nw["alignment"] == {"x": 1.5, "y": 8.5}, "Expected NW marker at 1.5,8.5" + assert pp_se["alignment"] == {"x": 8.5, "y": 1.5}, "Expected SE marker at 8.5,1.5" + assert len(pp_nw["sites"]) == 2, f"Expected to have exactly two 'NW' sites, got {pp_nw['sites']}" + assert len(pp_se["sites"]) == 2, f"Expected to have exactly two 'SE' sites, got {pp_se['sites']}" + site_is_at(pp_nw, "QB1_probe", 2.0, 5.0, 2.4, 5.0) + site_is_at(pp_se, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(pp_nw, "testarray_1_probe_0", 2.0, 8.6, 2.4, 8.6) + site_is_at(pp_se, "QB2_probe", 8.0, 5.0, 8.4, 5.0) + +def test_probepoints_with_multiple_references2(empty_cell): + add_refpoint(empty_cell, "QB1_probe_island_1", 2400, 2600) + add_refpoint(empty_cell, "QB1_probe_island_2", 2000, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_l", 8400, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 8000, 2600) + add_refpoint(empty_cell, "testarray_1_probe_0_l", 2400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 2000, 8600) + add_refpoint(empty_cell, "QB2_probe_ground", 8400, 8600) + add_refpoint(empty_cell, "QB2_probe_island", 8000, 8600) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'ne', 'sw', 'se']) + assert set(probepoints.keys()) == {"groups"}, "For multireference probepoint json expected to have 'groups' "\ + f"member on top-level, was {probepoints}" + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"] + pp_ne = [pp for pp in probepoints["groups"] if pp["id"] == "NE"] + pp_sw = [pp for pp in probepoints["groups"] if pp["id"] == "SW"] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"] + assert pp_nw, "No probepoint group extracted with id 'NW'" + assert pp_ne, "No probepoint group extracted with id 'NE'" + assert pp_sw, "No probepoint group extracted with id 'SW'" + assert pp_se, "No probepoint group extracted with id 'SE'" + pp_nw, pp_ne, pp_sw, pp_se = pp_nw[0], pp_ne[0], pp_sw[0], pp_se[0] + assert pp_nw["alignment"] == {"x": 1.5, "y": 8.5}, "Expected NW marker at 1.5,8.5" + assert pp_ne["alignment"] == {"x": 8.5, "y": 8.5}, "Expected NE marker at 8.5,8.5" + assert pp_sw["alignment"] == {"x": 1.5, "y": 1.5}, "Expected SW marker at 1.5,1.5" + assert pp_se["alignment"] == {"x": 8.5, "y": 1.5}, "Expected SE marker at 8.5,1.5" + assert len(pp_nw["sites"]) == 1, f"Expected to have exactly two 'NW' sites, got {pp_nw['sites']}" + assert len(pp_ne["sites"]) == 1, f"Expected to have exactly two 'NE' sites, got {pp_ne['sites']}" + assert len(pp_sw["sites"]) == 1, f"Expected to have exactly two 'SW' sites, got {pp_sw['sites']}" + assert len(pp_se["sites"]) == 1, f"Expected to have exactly two 'SE' sites, got {pp_se['sites']}" + site_is_at(pp_sw, "QB1_probe", 2.0, 2.6, 2.4, 2.6) + site_is_at(pp_se, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(pp_nw, "testarray_1_probe_0", 2.0, 8.6, 2.4, 8.6) + site_is_at(pp_ne, "QB2_probe", 8.0, 8.6, 8.4, 8.6) + +def test_probepoints_with_multiple_references_doesnt_separate_site(empty_cell): + add_refpoint(empty_cell, "testarray_2_probe_0_l", 8400, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 8000, 2600) + add_refpoint(empty_cell, "testarray_1_probe_0_l", 2400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 2000, 8600) + add_refpoint(empty_cell, "QB2_probe_ground", 4800, 4000) + add_refpoint(empty_cell, "QB2_probe_island", 5200, 4000) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'se']) + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"] + assert pp_nw, "No probepoint group extracted with id 'NW'" + assert pp_se, "No probepoint group extracted with id 'SE'" + pp_nw, pp_se = pp_nw[0], pp_se[0] + assert len(pp_nw["sites"]) == 1, f"Expected to have exactly two 'NW' sites, got {pp_nw['sites']}" + assert len(pp_se["sites"]) == 2, f"Expected to have exactly two 'SE' sites, got {pp_se['sites']}" + site_is_at(pp_se, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(pp_nw, "testarray_1_probe_0", 2.0, 8.6, 2.4, 8.6) + site_is_at(pp_se, "QB2_probe", 4.8, 4.0, 5.2, 4.0) + +def test_probepoints_with_multiple_references_remove_empty_groups(empty_cell): + add_refpoint(empty_cell, "QB1_probe_island_1", 2400, 6600) + add_refpoint(empty_cell, "QB1_probe_island_2", 2000, 6600) + add_refpoint(empty_cell, "testarray_2_probe_0_l", 8400, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 8000, 2600) + add_refpoint(empty_cell, "testarray_1_probe_0_l", 2400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 2000, 8600) + add_refpoint(empty_cell, "QB2_probe_ground", 8400, 8600) + add_refpoint(empty_cell, "QB2_probe_island", 8000, 8600) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'ne', 'sw', 'se']) + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"] + pp_ne = [pp for pp in probepoints["groups"] if pp["id"] == "NE"] + pp_sw = [pp for pp in probepoints["groups"] if pp["id"] == "SW"] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"] + assert pp_nw, "No probepoint group extracted with id 'NW'" + assert pp_ne, "No probepoint group extracted with id 'NE'" + assert not pp_sw, f"There should be not probepoint group extracted with id 'SW', got {pp_sw}" + assert pp_se, "No probepoint group extracted with id 'SE'" + pp_nw, pp_ne, pp_se = pp_nw[0], pp_ne[0], pp_se[0] + assert len(pp_nw["sites"]) == 2, f"Expected to have exactly two 'NW' sites, got {pp_nw['sites']}" + assert len(pp_ne["sites"]) == 1, f"Expected to have exactly two 'NE' sites, got {pp_ne['sites']}" + assert len(pp_se["sites"]) == 1, f"Expected to have exactly two 'SE' sites, got {pp_se['sites']}" + site_is_at(pp_nw, "QB1_probe", 2.0, 6.6, 2.4, 6.6) + site_is_at(pp_se, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(pp_nw, "testarray_1_probe_0", 2.0, 8.6, 2.4, 8.6) + site_is_at(pp_ne, "QB2_probe", 8.0, 8.6, 8.4, 8.6) + +def test_three_probe_points(dummy_cell, caplog): + add_refpoint(dummy_cell, "testarray_2_probe_0_l", 8000, 2600) + add_refpoint(dummy_cell, "testarray_2_probe_0_r", 8400, 2600) + add_refpoint(dummy_cell, "testarray_2_probe_0_top", 8200, 2700) + probepoints = generate_probepoints_json(dummy_cell) + assert not caplog.records, f"Didn't expect warnings but got following: {[x.message for x in caplog.records]}" + site_is_at(probepoints, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(probepoints, "testarray_2_probe_0_top_east", 8.2, 2.7, 8.4, 2.6) + site_is_at(probepoints, "testarray_2_probe_0_top_west", 8.0, 2.6, 8.2, 2.7) + +def test_three_probe_points_with_multiple_references(empty_cell): + add_refpoint(empty_cell, "testarray_2_probe_0_l", 4900, 4900) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 5100, 4900) + add_refpoint(empty_cell, "testarray_2_probe_0_top", 5000, 5100) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'se']) + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"] + assert pp_nw, "No probepoint group extracted with id 'NW'" + assert pp_se, "No probepoint group extracted with id 'SE'" + pp_nw, pp_se = pp_nw[0], pp_se[0] + site_is_at(pp_se, "testarray_2_probe_0", 4.9, 4.9, 5.1, 4.9) + site_is_at(pp_se, "testarray_2_probe_0_top_east", 5.0, 5.1, 5.1, 4.9) + site_is_at(pp_nw, "testarray_2_probe_0_top_west", 4.9, 4.9, 5.0, 5.1) + +def test_contact_pair_can_be_defined(dummy_cell): + probepoints = generate_probepoints_json(dummy_cell, contact=(pya.DPoint(2000, 3000), pya.DPoint(2100, 3000))) + assert len(probepoints["sites"]) == 2, f"Expected exactly two sites, got {probepoints}" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + site_is_at(probepoints, "contact", 2.0, 3.0, 2.1, 3.0) + +def test_contact_designates_west_east_top_face(dummy_cell): + probepoints = generate_probepoints_json(dummy_cell, face='2b1', + contact=(pya.DPoint(2000, 3000), pya.DPoint(2100, 3000))) + assert len(probepoints["sites"]) == 2, f"Expected exactly two sites, got {probepoints}" + top_chip_width, top_chip_offset_x, top_chip_offset_y = 7.0, 1.5, 1.5 + site_is_at(probepoints, "testarray_1_probe_0", + round(top_chip_width - (4.4 - top_chip_offset_x), 4), + round(8.6 - top_chip_offset_y, 4), + round(top_chip_width - (4.0 - top_chip_offset_x), 4), + round(8.6 - top_chip_offset_y, 4)) + site_is_at(probepoints, "contact", + round(top_chip_width - (2.1 - top_chip_offset_x), 4), + round(3.0 - top_chip_offset_y, 4), + round(top_chip_width - (2.0 - top_chip_offset_x), 4), + round(3.0 - top_chip_offset_y, 4)) + +def test_contact_designates_west_east_top_face_no_flip(dummy_cell): + probepoints = generate_probepoints_json(dummy_cell, face='2b1', flip_face=False, + contact=(pya.DPoint(2000, 3000), pya.DPoint(2100, 3000))) + assert len(probepoints["sites"]) == 2, f"Expected exactly two sites, got {probepoints}" + top_chip_offset_x, top_chip_offset_y = 1.5, 1.5 + site_is_at(probepoints, "testarray_1_probe_0", + round(4.0 - top_chip_offset_x, 4), + round(8.6 - top_chip_offset_y, 4), + round(4.4 - top_chip_offset_x, 4), + round(8.6 - top_chip_offset_y, 4)) + site_is_at(probepoints, "contact", + round(2.0 - top_chip_offset_x, 4), + round(3.0 - top_chip_offset_y, 4), + round(2.1 - top_chip_offset_x, 4), + round(3.0 - top_chip_offset_y, 4)) + +def test_one_contact_pair_for_multireference(empty_cell): + add_refpoint(empty_cell, "QB1_probe_island_1", 2400, 2600) + add_refpoint(empty_cell, "QB1_probe_island_2", 2000, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_l", 8400, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 8000, 2600) + add_refpoint(empty_cell, "testarray_1_probe_0_l", 2400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 2000, 8600) + add_refpoint(empty_cell, "QB2_probe_ground", 8400, 8600) + add_refpoint(empty_cell, "QB2_probe_island", 8000, 8600) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'ne', 'sw', 'se'], + contact=(pya.DPoint(2100, 7500), pya.DPoint(2300, 7500))) + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"][0] + pp_ne = [pp for pp in probepoints["groups"] if pp["id"] == "NE"][0] + pp_sw = [pp for pp in probepoints["groups"] if pp["id"] == "SW"][0] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"][0] + site_is_at(pp_nw, "testarray_1_probe_0", 2.0, 8.6, 2.4, 8.6) + site_is_at(pp_ne, "QB2_probe", 8.0, 8.6, 8.4, 8.6) + site_is_at(pp_sw, "QB1_probe", 2.0, 2.6, 2.4, 2.6) + site_is_at(pp_se, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(pp_nw, "contact", 2.1, 7.5, 2.3, 7.5) + site_is_at(pp_ne, "contact", 2.1, 7.5, 2.3, 7.5) + site_is_at(pp_sw, "contact", 2.1, 7.5, 2.3, 7.5) + site_is_at(pp_se, "contact", 2.1, 7.5, 2.3, 7.5) + +def test_multiple_contacts_for_multireference(empty_cell): + add_refpoint(empty_cell, "QB1_probe_island_1", 2400, 2600) + add_refpoint(empty_cell, "QB1_probe_island_2", 2000, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_l", 8400, 2600) + add_refpoint(empty_cell, "testarray_2_probe_0_r", 8000, 2600) + add_refpoint(empty_cell, "testarray_1_probe_0_l", 2400, 8600) + add_refpoint(empty_cell, "testarray_1_probe_0_r", 2000, 8600) + add_refpoint(empty_cell, "QB2_probe_ground", 8400, 8600) + add_refpoint(empty_cell, "QB2_probe_island", 8000, 8600) + probepoints = generate_probepoints_json(empty_cell, references=['nw', 'ne', 'sw', 'se'], contact=[ + (pya.DPoint(8200, 3400), pya.DPoint(8400, 3400)), + (pya.DPoint(2200, 3500), pya.DPoint(2400, 3500)), + (pya.DPoint(8100, 7400), pya.DPoint(8300, 7400)), + (pya.DPoint(2100, 7500), pya.DPoint(2300, 7500)), + ]) + pp_nw = [pp for pp in probepoints["groups"] if pp["id"] == "NW"][0] + pp_ne = [pp for pp in probepoints["groups"] if pp["id"] == "NE"][0] + pp_sw = [pp for pp in probepoints["groups"] if pp["id"] == "SW"][0] + pp_se = [pp for pp in probepoints["groups"] if pp["id"] == "SE"][0] + site_is_at(pp_nw, "testarray_1_probe_0", 2.0, 8.6, 2.4, 8.6) + site_is_at(pp_ne, "QB2_probe", 8.0, 8.6, 8.4, 8.6) + site_is_at(pp_sw, "QB1_probe", 2.0, 2.6, 2.4, 2.6) + site_is_at(pp_se, "testarray_2_probe_0", 8.0, 2.6, 8.4, 2.6) + site_is_at(pp_nw, "contact", 8.2, 3.4, 8.4, 3.4) + site_is_at(pp_ne, "contact", 2.2, 3.5, 2.4, 3.5) + site_is_at(pp_sw, "contact", 8.1, 7.4, 8.3, 7.4) + site_is_at(pp_se, "contact", 2.1, 7.5, 2.3, 7.5) + +def test_duplicate_sites_removed(empty_cell, caplog): + add_refpoint(empty_cell, "testarray_short_name_l", 4400, 2600) + add_refpoint(empty_cell, "testarray_short_name_r", 4000, 2600) + add_refpoint(empty_cell, "testarray_veery_loong_name_l", 4000, 2600) + add_refpoint(empty_cell, "testarray_veery_loong_name_r", 4400, 2600) + probepoints = generate_probepoints_json(empty_cell) + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 4, f"Expected to have four row warning in the log, got: {log_messages}" + assert "Found two sites " in log_messages[0] and \ + "'testarray_short_name'" in log_messages[0] and \ + "'testarray_veery_loong_name'" in log_messages[0] and \ + " with similar coordinates (respectively)" in log_messages[0],\ + f"Unexpected warning message: {log_messages}" + assert " west 4.0,2.6 = 4.0,2.6" in log_messages[1], f"Unexpected warning message: {log_messages}" + assert " east 4.4,2.6 = 4.4,2.6" in log_messages[2], f"Unexpected warning message: {log_messages}" + assert " will only keep the site 'testarray_veery_loong_name'" in log_messages[3],\ + f"Unexpected warning message: {log_messages}" + assert len(probepoints["sites"]) == 1, f"Expected exactly one site, got {probepoints}" + site_is_at(probepoints, "testarray_veery_loong_name", 4.0, 2.6, 4.4, 2.6) + +def test_close_sites_removed(empty_cell, caplog): + add_refpoint(empty_cell, "testarray_short_name_l", 4400.1, 2600) + add_refpoint(empty_cell, "testarray_short_name_r", 4000, 2600.1) + add_refpoint(empty_cell, "testarray_veery_loong_name_l", 4000.1, 2600) + add_refpoint(empty_cell, "testarray_veery_loong_name_r", 4400, 2600.1) + probepoints = generate_probepoints_json(empty_cell) + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 4, f"Expected to have four row warning in the log, got: {log_messages}" + assert "Found two sites " in log_messages[0] and \ + "'testarray_short_name'" in log_messages[0] and \ + "'testarray_veery_loong_name'" in log_messages[0] and \ + " with similar coordinates (respectively)" in log_messages[0],\ + f"Unexpected warning message: {log_messages}" + assert " west " in log_messages[1] and \ + "4.0,2.6001" in log_messages[1] and \ + "4.0001,2.6" in log_messages[1], f"Unexpected warning message: {log_messages}" + assert " east " in log_messages[2] and \ + "4.4001,2.6" in log_messages[2] and \ + "4.4,2.6001" in log_messages[2], f"Unexpected warning message: {log_messages}" + assert " will only keep the site 'testarray_veery_loong_name'" in log_messages[3],\ + f"Unexpected warning message: {log_messages}" + assert len(probepoints["sites"]) == 1, f"Expected exactly one site, got {probepoints}" + site_is_at(probepoints, "testarray_veery_loong_name", 4.0001, 2.6, 4.4, 2.6001) + +def test_far_enough_sites_stay(empty_cell, caplog): + add_refpoint(empty_cell, "testarray_short_name_l", 4402, 2600) + add_refpoint(empty_cell, "testarray_short_name_r", 4000, 2602) + add_refpoint(empty_cell, "testarray_veery_loong_name_l", 4002, 2600) + add_refpoint(empty_cell, "testarray_veery_loong_name_r", 4400, 2602) + probepoints = generate_probepoints_json(empty_cell) + log_messages = [x.message for x in caplog.records] + assert not log_messages, f"Unexpected warnings in the log: {log_messages}" + assert len(probepoints["sites"]) == 2, f"Expected two distinct sites, got {probepoints}" + site_is_at(probepoints, "testarray_veery_loong_name", 4.002, 2.6, 4.4, 2.602) + site_is_at(probepoints, "testarray_short_name", 4.0, 2.602, 4.402, 2.6) + +def test_warn_if_only_one_probepoint_per_site(dummy_cell, caplog): + add_refpoint(dummy_cell, "testarray_solitary_r", 5000, 5000) + probepoints = generate_probepoints_json(dummy_cell) + log_messages = [x.message for x in caplog.records] + assert len(log_messages) == 1, "Expected exactly one warning about site with one probepoint" + assert "Malformed site object detected: " in log_messages[0] and \ + "'id': 'testarray_solitary'" in log_messages[0], f"Unexpected warning: {log_messages[0]}" + site_is_at(probepoints, "testarray_1_probe_0", 4.0, 8.6, 4.4, 8.6) + site_is_at(probepoints, "testarray_solitary", 5.0, 5.0, 5.0, 5.0)