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)