From 8bffb5208397a5ede5ba6096705a57e8fda14f15 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 29 Jul 2021 11:21:21 +0200 Subject: [PATCH 1/2] Add failing test case when reusing cells in a parallelized factory function --- gdshelpers/tests/test_gdsii_export_import.py | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/gdshelpers/tests/test_gdsii_export_import.py b/gdshelpers/tests/test_gdsii_export_import.py index 2b9891e..b32ee08 100644 --- a/gdshelpers/tests/test_gdsii_export_import.py +++ b/gdshelpers/tests/test_gdsii_export_import.py @@ -5,9 +5,21 @@ from gdshelpers.parts.waveguide import Waveguide from gdshelpers.geometry.chip import Cell +from gdshelpers.parts.coupler import GratingCoupler +from gdshelpers.parts.port import Port from gdshelpers.parts.pattern_import import GDSIIImport +def make_test_cell(args): + # Helper function for the test case 'test_parallel'. + # Needs to be global so that it can be pickled for parallel execution + i, gc_cell, port = args + cell = Cell('complex_cell_{}'.format(i)) + cell.add_to_layer(1, Waveguide.make_at_port(port).add_straight_segment(10)) + cell.add_cell(gc_cell, [0, 0]) + return i, cell + + class GdsTestCase(unittest.TestCase): def test_export_import(self): waveguide = Waveguide([0, 0], 0, 1) @@ -60,3 +72,36 @@ def test_parallel_export(self): cells[0].save('parallel.gds', parallel=True) self.assertTrue(filecmp.cmp('serial.gds', 'parallel.gds')) + + def test_parallel_cell_reuse(self): + """ + This test case tests if it is possible to generate Cells in parallel when they + reuse a common sub cell. + This can be useful when creating a cell is an expensive operation and should + be parallelized, but some components (such as grating couplers) can be reused. + """ + from concurrent.futures import ProcessPoolExecutor + port = Port([0, 0], 0, 1.) + + # Create a common cell for grating couplers, which is reused + gc_cell = Cell("gc") + gc = GratingCoupler.make_traditional_coupler_at_port(port.inverted_direction, + full_opening_angle=np.deg2rad(40), + grating_period=1.13, + grating_ff=0.85, + n_gratings=20) + gc_cell.add_to_layer(1, gc) + + top_cell = Cell("top") + with ProcessPoolExecutor(max_workers=4) as executor: + for i, cell in executor.map(make_test_cell, [(i, gc_cell, port) for i in range(8)]): + top_cell.add_cell(cell, origin=[i*100, 0]) + + top_cell.save("test_parallel.gds") + + # However, adding two different cells with the same name should still be prohibited + top_cell.add_cell(Cell("foo")) + top_cell.add_cell(Cell("foo")) + + with self.assertRaises(AssertionError): + top_cell.save("test_parallel.gds") From 2b50fffee92480043d05430ac70a522a6e3e8136 Mon Sep 17 00:00:00 2001 From: fbeutel Date: Thu, 29 Jul 2021 11:22:35 +0200 Subject: [PATCH 2/2] Identify cells by uuid when they are created so that they can be identified even after they have been pickled --- gdshelpers/export/gdsii_export.py | 23 ++++++++++++++--------- gdshelpers/geometry/chip.py | 5 +++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/gdshelpers/export/gdsii_export.py b/gdshelpers/export/gdsii_export.py index dd24058..6cc2053 100644 --- a/gdshelpers/export/gdsii_export.py +++ b/gdshelpers/export/gdsii_export.py @@ -95,17 +95,22 @@ def write_cell_to_gdsii_file(outfile, cell, unit=1e-6, grid_steps_per_unit=1000, grid_step_unit = unit / grid_steps_per_unit timestamp = datetime.datetime.now() if timestamp is None else timestamp - cells = [] - cell_names = [] + cells = dict() + + def cells_equivalent(cell_a, cell_b): + if cell_a._uuid is None: + return cell_a == cell_b + else: + return cell_a._uuid == cell_b._uuid def add_cells_to_unique_list(start_cell): - cells.append(start_cell) - cell_names.append(start_cell.name) + cells[start_cell.name] = start_cell for c in start_cell.cells: - if c['cell'] not in cells: - if c['cell'].name in cell_names: + if c['cell'].name in cells: + if not cells_equivalent(c['cell'], cells[c['cell'].name]): raise AssertionError( 'Each cell name must be unique, "{}" is used more than once'.format(c['cell'].name)) + else: add_cells_to_unique_list(c['cell']) add_cells_to_unique_list(cell) @@ -121,11 +126,11 @@ def add_cells_to_unique_list(start_cell): from concurrent.futures import ProcessPoolExecutor with ProcessPoolExecutor(max_workers=max_workers) as pool: num = len(cells) - for binary in pool.map(_cell_to_gdsii_binary, cells, (grid_steps_per_unit,) * num, (max_points,) * num, - (max_line_points,) * num, (timestamp,) * num): + for binary in pool.map(_cell_to_gdsii_binary, cells.values(), (grid_steps_per_unit,) * num, + (max_points,) * num, (max_line_points,) * num, (timestamp,) * num): outfile.write(binary) else: - for c in cells: + for c in cells.values(): outfile.write(_cell_to_gdsii_binary(c, grid_steps_per_unit, max_points, max_line_points, timestamp)) outfile.write(pack('>2H', 4, 0x0400)) # ENDLIB N0_DATA diff --git a/gdshelpers/geometry/chip.py b/gdshelpers/geometry/chip.py index 0532268..135c938 100644 --- a/gdshelpers/geometry/chip.py +++ b/gdshelpers/geometry/chip.py @@ -10,6 +10,7 @@ from gdshelpers.geometry import geometric_union from gdshelpers.parts.port import Port import gdshelpers.helpers.layers as std_layers +import uuid class Cell: @@ -32,6 +33,10 @@ def __init__(self, name: str): # self._bounds is None if the bounds need to be recalculated or if the cell is empty (in which case # recalculating them is cheap and we don't need to cache them) + self._uuid = uuid.uuid4() + # assign a unique UUID for this cell such that we can identify cells which have been pickled or + # serialized. + @property def bounds(self): """