from __future__ import annotations
import itertools
from numpy import float64
import gdsfactory as gf
from gdsfactory.component import Component
from gdsfactory.components.coupler import coupler
from gdsfactory.components.crossing_waveguide import compensation_path, crossing45
from gdsfactory.port import get_ports_facing
COUNTER = itertools.count()
def gen_tmp_port_name() -> str:
return f"{next(COUNTER)}"
def swap(straights, i, j):
a = straights[i]
straights[i] = straights[j]
straights[j] = a
return straights
def dist(i, wgs1, wgs2):
a = wgs1.index(i)
b = wgs2.index(i)
return b - a
def get_sequence_cross(
straights_start, straights_end, iter_max: int = 100, symbols=("X", "-")
):
"""Returns sequence of crossings to achieve the permutations between two columns of I/O.
Args:
straights_start: list of the input port indices.
straights_end: list of the output port indices.
iter_max: maximum iterations.
symbols: [`X` , `S`].
Notes:
symbols to be used in the returned sequence:
- `X`:represents the crossing symbol: two Xs next to each-other means that the two
modes have to be swapped
- `S`:Straight straight, or compensation path typically
"""
wgs = list(straights_start)
straights_end = list(straights_end)
N = len(wgs)
sequence = []
X, S = symbols # Cross, Straight symbols
nb_iters = 0
while wgs != straights_end:
if nb_iters > iter_max:
print(
"Exceeded max number of iterations. The following I/O are mismatched:"
)
for i in range(len(straights_end)):
print(wgs[i], "<->", straights_end[i])
return sequence
if nb_iters > 2 and sequence[-1] == sequence[-2]:
print("Two consecutive sequences are the same. Got stuck")
return sequence
swaps = []
i = 0
# total_dist = 0
while i < N - 1:
a = wgs[i]
b = wgs[i + 1]
d1 = dist(a, wgs, straights_end)
d2 = dist(b, wgs, straights_end)
# total_dist += abs(d1) + abs(d2)
# The equality cases are very important:
# if one straight needs to cross, then even if the other one is
# already at the right place, it must swap to allow the other one
# to cross
if d1 >= 0 and d2 <= 0 and (d1 != 0 or d2 != 0):
wgs = swap(wgs, i, i + 1)
swaps += [X, X]
i += 1
else:
# Edge case if only one wg remain it is straight
swaps += [S]
# We cannot swap twice the same straight on the same iteration, so we
# skip the next straight by incrementing
# Edge case: Cannot swap if only one wg left so it has to be a straight
if i == N - 2:
swaps += [S]
i += 1
sequence.append(swaps)
nb_iters += 1
return sequence
def component_sequence_to_str(sequence):
"""Transform a sequence of components (such as the one obtained from.
get_sequence_cross_str) into an ASCII block which can be used either as
a cartoon or as an input for component_lattice(lattice = ...)
"""
component_txt_lattice = ""
M = len(sequence[0])
for i in range(M):
j = M - 1 - i
line = "".join(col[j] for col in sequence) + "\n"
component_txt_lattice += line
return component_txt_lattice
def get_sequence_cross_str(straights_start, straights_end, iter_max: int = 100):
seq = get_sequence_cross(
straights_start, straights_end, iter_max=iter_max, symbols=["X", "-"]
)
return component_sequence_to_str(seq)
[docs]
@gf.cell
def component_lattice(
lattice: str = """
C-X
CXX
CXX
C-X
""",
symbol_to_component: dict[str, Component] | None = None,
grid_per_unit: int = 1000,
) -> Component:
"""Return a lattice Component of N inputs and outputs Columns must have.
components with the same x spacing between input/output ports Lines must
have components with the same y spacing between input/output ports.
Args:
lattice: ASCII map with character.
symbol_to_component: dict of ASCII character to component.
grid_per_unit: int.
Lattice example:
.. code::
X-X
XCX
XCX
X-X
.. plot::
:include-source:
import gdsfactory as gf
from gdsfactory.components.crossing_waveguide import crossing45
from gdsfactory.components.crossing_waveguide import compensation_path
symbol_to_component = {
"C": gf.routing.fanout2x2(component=gf.components.coupler(), port_spacing=40.0),
"X": crossing45(port_spacing=40.0),
"-": compensation_path(crossing45=crossing45(port_spacing=40.0)),
}
c = gf.components.component_lattice(symbol_to_component=symbol_to_component)
c.plot()
"""
x = crossing45(port_spacing=40)
symbol_to_component = symbol_to_component or {
"C": gf.routing.fanout2x2(component=coupler(), port_spacing=40.0),
"X": x,
"-": compensation_path(x),
}
# Find y spacing and check that all components have same y spacing
y_spacing = None
for component in symbol_to_component.values():
component = gf.get_component(component)
# component = component.copy()
# component.auto_rename_ports_orientation()
for direction in ["W", "E"]:
ports_dir = get_ports_facing(component.ports, direction)
ports_dir.sort(key=lambda p: p.y)
nb_ports = len(ports_dir)
if nb_ports > 1:
_y_spacing = (ports_dir[-1].y - ports_dir[0].y) / (nb_ports - 1)
if y_spacing is None:
y_spacing = _y_spacing
else:
assert abs(y_spacing - _y_spacing) < 0.1 / grid_per_unit, (
"All component must have the same y port spacing. Got"
f" {y_spacing}, {_y_spacing} for {component.name}"
)
a = y_spacing
columns, columns_to_length = parse_lattice(lattice, symbol_to_component)
keys = sorted(columns.keys())
components_to_nb_input_ports = {
c: len(get_ports_facing(symbol_to_component[c], "W"))
for c in symbol_to_component.keys()
}
component = gf.Component()
x = 0
for i in keys:
col = columns[i]
j = 0
L = columns_to_length[i]
skip = 0 # number of lines to skip depending on the number of ports
for c in col:
y = -j * a
if skip == 1:
j += skip
skip = 0
continue
if c in symbol_to_component.keys():
# Compute the number of ports to skip: They will already be
# connected since they belong to this component
nb_inputs = components_to_nb_input_ports[c]
skip = nb_inputs - 1
ports_cw = symbol_to_component[c].get_ports_list(clockwise=True)
_cmp = symbol_to_component[c].ref((x, y), port_id=ports_cw[skip].name)
# _cmp = symbol_to_component[c].ref((x, y), port_id="oW{}".format(skip))
component.add(_cmp)
if i == 0:
_ports = get_ports_facing(_cmp, "W")
for _p in _ports:
component.add_port(gen_tmp_port_name(), port=_p)
if i == keys[-1]:
_ports = get_ports_facing(_cmp, "E")
for _p in _ports:
component.add_port(gen_tmp_port_name(), port=_p)
else:
symbols = list(symbol_to_component.keys())
raise ValueError(
f"symbol {c!r} not in symbol_to_component dict {symbols}"
)
j += 1
x += L
component.auto_rename_ports()
return component
def parse_lattice(
lattice: str, symbol_to_component: dict[str, Component]
) -> tuple[dict[int, list[str]], dict[int, float64]]:
"""Extract each column.
Args:
lattice: string describing lattice.
symbol_to_component: dict of ASCII character to component.
"""
lines = lattice.replace(" ", "").split("\n")
columns = {}
columns_to_length = {}
for line in lines:
if len(line) > 0:
for i, c in enumerate(line):
if i not in columns.keys():
columns[i] = []
columns[i].append(c)
if c in symbol_to_component:
cmp = symbol_to_component[c]
pcw = cmp.get_ports_list(clockwise=True)
pccw = cmp.get_ports_list(clockwise=False)
# columns_to_length[i] = cmp.ports["oE0"].x - cmp.ports["oW0"].x
columns_to_length[i] = (
cmp.ports[pccw[0].name].x - cmp.ports[pcw[0].name].x
)
return columns, columns_to_length
if __name__ == "__main__":
# components_dict = {
# "C": gf.routing.fanout2x2(component=gf.components.coupler(), port_spacing=40.0),
# "X": crossing45(port_spacing=40.0),
# "-": compensation_path(crossing45=crossing45(port_spacing=40.0)),
# }
# c = gf.components.component_lattice(symbol_to_component=components_dict)
# c= gf.routing.fanout2x2(component=gf.components.coupler(), port_spacing=40.0)
# c= crossing45(port_spacing=40.0)
# c = compensation_path(crossing45=crossing45(port_spacing=40.0))
# c.pprint_ports()
# c = compensation_path()
c = component_lattice()
c.show(show_ports=True)