librelane 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of librelane might be problematic. Click here for more details.

Files changed (170) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +479 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +63 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +246 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +456 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +116 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +155 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +743 -0
  27. librelane/container.py +285 -0
  28. librelane/env_info.py +320 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +327 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +1049 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/help/__main__.py +39 -0
  52. librelane/logging/__init__.py +40 -0
  53. librelane/logging/logger.py +323 -0
  54. librelane/open_pdks_rev +1 -0
  55. librelane/plugins.py +21 -0
  56. librelane/py.typed +0 -0
  57. librelane/scripts/base.sdc +80 -0
  58. librelane/scripts/klayout/Readme.md +2 -0
  59. librelane/scripts/klayout/open_design.py +63 -0
  60. librelane/scripts/klayout/render.py +121 -0
  61. librelane/scripts/klayout/stream_out.py +176 -0
  62. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  63. librelane/scripts/klayout/xor.drc +120 -0
  64. librelane/scripts/magic/Readme.md +1 -0
  65. librelane/scripts/magic/common/read.tcl +114 -0
  66. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  67. librelane/scripts/magic/def/mag.tcl +19 -0
  68. librelane/scripts/magic/def/mag_gds.tcl +79 -0
  69. librelane/scripts/magic/drc.tcl +78 -0
  70. librelane/scripts/magic/extract_spice.tcl +98 -0
  71. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  72. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  73. librelane/scripts/magic/gds/extras_mag.tcl +45 -0
  74. librelane/scripts/magic/gds/mag_with_pointers.tcl +31 -0
  75. librelane/scripts/magic/get_bbox.tcl +11 -0
  76. librelane/scripts/magic/lef/extras_maglef.tcl +61 -0
  77. librelane/scripts/magic/lef/maglef.tcl +26 -0
  78. librelane/scripts/magic/lef.tcl +57 -0
  79. librelane/scripts/magic/open.tcl +28 -0
  80. librelane/scripts/magic/wrapper.tcl +21 -0
  81. librelane/scripts/netgen/setup.tcl +28 -0
  82. librelane/scripts/odbpy/apply_def_template.py +49 -0
  83. librelane/scripts/odbpy/cell_frequency.py +107 -0
  84. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  85. librelane/scripts/odbpy/contextualize.py +109 -0
  86. librelane/scripts/odbpy/defutil.py +573 -0
  87. librelane/scripts/odbpy/diodes.py +373 -0
  88. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  89. librelane/scripts/odbpy/eco_buffer.py +181 -0
  90. librelane/scripts/odbpy/eco_diode.py +139 -0
  91. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  92. librelane/scripts/odbpy/io_place.py +482 -0
  93. librelane/scripts/odbpy/ioplace_parser/__init__.py +23 -0
  94. librelane/scripts/odbpy/ioplace_parser/parse.py +147 -0
  95. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  96. librelane/scripts/odbpy/lefutil.py +97 -0
  97. librelane/scripts/odbpy/placers.py +162 -0
  98. librelane/scripts/odbpy/power_utils.py +397 -0
  99. librelane/scripts/odbpy/random_place.py +57 -0
  100. librelane/scripts/odbpy/reader.py +250 -0
  101. librelane/scripts/odbpy/remove_buffers.py +173 -0
  102. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  103. librelane/scripts/odbpy/wire_lengths.py +93 -0
  104. librelane/scripts/openroad/antenna_check.tcl +20 -0
  105. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  106. librelane/scripts/openroad/basic_mp.tcl +24 -0
  107. librelane/scripts/openroad/buffer_list.tcl +10 -0
  108. librelane/scripts/openroad/common/dpl.tcl +24 -0
  109. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  110. librelane/scripts/openroad/common/grt.tcl +32 -0
  111. librelane/scripts/openroad/common/io.tcl +540 -0
  112. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  113. librelane/scripts/openroad/common/resizer.tcl +103 -0
  114. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  115. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  116. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  117. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  118. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  119. librelane/scripts/openroad/cts.tcl +80 -0
  120. librelane/scripts/openroad/cut_rows.tcl +24 -0
  121. librelane/scripts/openroad/dpl.tcl +24 -0
  122. librelane/scripts/openroad/drt.tcl +37 -0
  123. librelane/scripts/openroad/fill.tcl +30 -0
  124. librelane/scripts/openroad/floorplan.tcl +145 -0
  125. librelane/scripts/openroad/gpl.tcl +88 -0
  126. librelane/scripts/openroad/grt.tcl +30 -0
  127. librelane/scripts/openroad/gui.tcl +37 -0
  128. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  129. librelane/scripts/openroad/ioplacer.tcl +67 -0
  130. librelane/scripts/openroad/irdrop.tcl +51 -0
  131. librelane/scripts/openroad/pdn.tcl +52 -0
  132. librelane/scripts/openroad/rcx.tcl +32 -0
  133. librelane/scripts/openroad/repair_design.tcl +70 -0
  134. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  135. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  136. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  137. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  138. librelane/scripts/openroad/sta/corner.tcl +393 -0
  139. librelane/scripts/openroad/tapcell.tcl +25 -0
  140. librelane/scripts/openroad/write_views.tcl +27 -0
  141. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  142. librelane/scripts/pyosys/json_header.py +84 -0
  143. librelane/scripts/pyosys/synthesize.py +493 -0
  144. librelane/scripts/pyosys/ys_common.py +153 -0
  145. librelane/scripts/tclsh/hello.tcl +1 -0
  146. librelane/state/__init__.py +24 -0
  147. librelane/state/__main__.py +61 -0
  148. librelane/state/design_format.py +195 -0
  149. librelane/state/state.py +359 -0
  150. librelane/steps/__init__.py +61 -0
  151. librelane/steps/__main__.py +510 -0
  152. librelane/steps/checker.py +637 -0
  153. librelane/steps/common_variables.py +340 -0
  154. librelane/steps/cvc_rv.py +169 -0
  155. librelane/steps/klayout.py +509 -0
  156. librelane/steps/magic.py +576 -0
  157. librelane/steps/misc.py +160 -0
  158. librelane/steps/netgen.py +253 -0
  159. librelane/steps/odb.py +1088 -0
  160. librelane/steps/openroad.py +2460 -0
  161. librelane/steps/openroad_alerts.py +102 -0
  162. librelane/steps/pyosys.py +640 -0
  163. librelane/steps/step.py +1571 -0
  164. librelane/steps/tclstep.py +288 -0
  165. librelane/steps/verilator.py +222 -0
  166. librelane/steps/yosys.py +371 -0
  167. librelane-2.4.0.dist-info/METADATA +169 -0
  168. librelane-2.4.0.dist-info/RECORD +170 -0
  169. librelane-2.4.0.dist-info/WHEEL +4 -0
  170. librelane-2.4.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,482 @@
1
+ # Copyright 2020-2022 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from functools import partial
15
+ import odb
16
+
17
+ import os
18
+ import re
19
+ import sys
20
+ import math
21
+ import click
22
+ import random
23
+ from decimal import Decimal
24
+
25
+ from reader import click_odb
26
+ import ioplace_parser
27
+
28
+
29
+ def grid_to_tracks(origin, count, step):
30
+ tracks = []
31
+ pos = origin
32
+ for _ in range(count):
33
+ tracks.append(pos)
34
+ pos += step
35
+ assert len(tracks) > 0
36
+ tracks.sort()
37
+
38
+ return tracks
39
+
40
+
41
+ def equally_spaced_sequence(side, side_pin_placement, possible_locations):
42
+ virtual_pin_count = 0
43
+ actual_pin_count = len(side_pin_placement)
44
+ total_pin_count = actual_pin_count + virtual_pin_count
45
+ for i in range(len(side_pin_placement)):
46
+ if isinstance(
47
+ side_pin_placement[i], int
48
+ ): # This is an int value indicating virtual pins
49
+ virtual_pin_count = virtual_pin_count + side_pin_placement[i]
50
+ actual_pin_count = (
51
+ actual_pin_count - 1
52
+ ) # Decrement actual pin count, this value was only there to indicate virtual pin count
53
+ total_pin_count = actual_pin_count + virtual_pin_count
54
+ result = []
55
+ tracks = len(possible_locations)
56
+
57
+ if total_pin_count > tracks:
58
+ print(
59
+ f"[ERROR] The {side} side of the floorplan doesn't have enough slots for all the pins: {total_pin_count} pins/{tracks} slots.",
60
+ file=sys.stderr,
61
+ )
62
+ print(
63
+ "[INFO] Try re-assigning pins to other sides or making the floorplan larger.",
64
+ file=sys.stderr,
65
+ )
66
+ sys.exit(1)
67
+ elif total_pin_count == tracks:
68
+ return possible_locations, side_pin_placement # All positions.
69
+ elif total_pin_count == 0:
70
+ return result, side_pin_placement
71
+
72
+ # From this point, pin_count always < tracks.
73
+ tracks_per_pin = math.floor(tracks / total_pin_count) # >=1
74
+ # O| | | O| | | O| | |
75
+ # tracks_per_pin = 3
76
+ # notice the last two tracks are unused
77
+ # thus:
78
+ used_tracks = tracks_per_pin * (total_pin_count - 1) + 1
79
+ unused_tracks = tracks - used_tracks
80
+
81
+ # Place the pins at those tracks...
82
+ current_track = unused_tracks // 2 # So that the tracks used are centered
83
+ starting_track_index = current_track
84
+ if virtual_pin_count == 0: # No virtual pins
85
+ for _ in range(0, total_pin_count):
86
+ result.append(possible_locations[current_track])
87
+ current_track += tracks_per_pin
88
+ else: # There are virtual pins
89
+ for i in range(len(side_pin_placement)):
90
+ if not isinstance(side_pin_placement[i], int): # We have an actual pin
91
+ result.append(possible_locations[current_track])
92
+ current_track += tracks_per_pin
93
+ else: # Virtual Pins, so just leave their needed spaces
94
+ current_track += tracks_per_pin * side_pin_placement[i]
95
+ side_pin_placement = [
96
+ pin for pin in side_pin_placement if not isinstance(pin, int)
97
+ ] # Remove the virtual pins from the side_pin_placement list
98
+
99
+ print(f"Placement details for the {side} side")
100
+ print("Virtual pin count: ", virtual_pin_count)
101
+ print("Actual pin count: ", actual_pin_count)
102
+ print("Total pin count: ", total_pin_count)
103
+ print("Tracks count: ", len(possible_locations))
104
+ print("Tracks per pin: ", tracks_per_pin)
105
+ print("Used tracks count: ", used_tracks)
106
+ print("Unused track count: ", unused_tracks)
107
+ print("Starting track index: ", starting_track_index)
108
+
109
+ VISUALIZE_PLACEMENT = False
110
+ if VISUALIZE_PLACEMENT:
111
+ print("Placement Map:")
112
+ print("[", end="")
113
+ used_track_indices = []
114
+ for i, location in enumerate(possible_locations):
115
+ if location in result:
116
+ print(f"\033[91m{location}\033[0m, ", end="")
117
+ used_track_indices.append(i)
118
+ else:
119
+ print(f"{location}, ", end="")
120
+ print("]")
121
+ print(f"Indices of used tracks: {used_track_indices}")
122
+ print("---")
123
+
124
+ return result, side_pin_placement
125
+
126
+
127
+ identifiers = re.compile(r"\b[A-Za-z_][A-Za-z_0-9]*\b")
128
+ standalone_numbers = re.compile(r"\b\d+\b")
129
+ trash = re.compile(r"^[^\w\d]+$")
130
+
131
+
132
+ def sorter(bterm, order: ioplace_parser.Order):
133
+ text: str = bterm.getName()
134
+ keys = []
135
+ priority_keys = []
136
+ # tokenize and add to key
137
+ while trash.match(text) is None:
138
+ if match := identifiers.search(text):
139
+ bus = match[0]
140
+ start, end = match.span(0)
141
+ if order == ioplace_parser.Order.busMajor:
142
+ priority_keys.append(bus)
143
+ else:
144
+ keys.append(bus)
145
+ text = text[:start] + text[end + 1 :]
146
+ elif match := standalone_numbers.search(text):
147
+ index = int(match[0])
148
+ if order == ioplace_parser.Order.bitMajor:
149
+ priority_keys.append(index)
150
+ else:
151
+ keys.append(index)
152
+ text = text[: match.pos] + text[match.endpos + 1 :]
153
+ else:
154
+ break
155
+ return [priority_keys, keys]
156
+
157
+
158
+ @click.command()
159
+ @click.option(
160
+ "-u",
161
+ "--unmatched-error",
162
+ type=click.Choice(["none", "unmatched_design", "unmatched_cfg", "both"]),
163
+ default=True,
164
+ help="Treat unmatched pins as error",
165
+ )
166
+ @click.option(
167
+ "-c",
168
+ "--config",
169
+ required=True,
170
+ type=click.Path(
171
+ exists=True,
172
+ file_okay=True,
173
+ dir_okay=False,
174
+ readable=True,
175
+ resolve_path=True,
176
+ ),
177
+ help="Input configuration file",
178
+ )
179
+ @click.option(
180
+ "-v",
181
+ "--ver-length",
182
+ default=None,
183
+ type=float,
184
+ help="Length for pins with N/S orientations in microns.",
185
+ )
186
+ @click.option(
187
+ "-h",
188
+ "--hor-length",
189
+ default=None,
190
+ type=float,
191
+ help="Length for pins with E/S orientations in microns.",
192
+ )
193
+ @click.option(
194
+ "-V",
195
+ "--ver-layer",
196
+ required=True,
197
+ help="Name of metal layer to place vertical pins on.",
198
+ )
199
+ @click.option(
200
+ "-H",
201
+ "--hor-layer",
202
+ required=True,
203
+ help="Name of metal layer to place horizontal pins on.",
204
+ )
205
+ @click.option(
206
+ "--hor-extension",
207
+ default=0,
208
+ type=float,
209
+ help="Extension for vertical pins in microns.",
210
+ )
211
+ @click.option(
212
+ "--ver-extension",
213
+ default=0,
214
+ type=float,
215
+ help="Extension for horizontal pins in microns.",
216
+ )
217
+ @click.option(
218
+ "--ver-width-mult", default=2, type=float, help="Multiplier for vertical pins."
219
+ )
220
+ @click.option(
221
+ "--hor-width-mult", default=2, type=float, help="Multiplier for horizontal pins."
222
+ )
223
+ @click_odb
224
+ def io_place(
225
+ reader,
226
+ config,
227
+ ver_layer,
228
+ hor_layer,
229
+ ver_width_mult,
230
+ hor_width_mult,
231
+ hor_length,
232
+ ver_length,
233
+ hor_extension,
234
+ ver_extension,
235
+ unmatched_error,
236
+ ):
237
+ """
238
+ Places the IOs in an input def with an optional config file that supports regexes.
239
+
240
+ Config format:
241
+ #N|#S|#E|#W
242
+ pin1_regex (low co-ordinates to high co-ordinates; e.g., bottom to top and left to right)
243
+ pin2_regex
244
+ ...
245
+
246
+ #S|#N|#E|#W
247
+ """
248
+ config_file_name = config
249
+ micron_in_units = reader.dbunits
250
+
251
+ H_EXTENSION = int(micron_in_units * hor_extension)
252
+ V_EXTENSION = int(micron_in_units * ver_extension)
253
+
254
+ if H_EXTENSION < 0:
255
+ H_EXTENSION = 0
256
+
257
+ if V_EXTENSION < 0:
258
+ V_EXTENSION = 0
259
+
260
+ H_LAYER = reader.tech.findLayer(hor_layer)
261
+ V_LAYER = reader.tech.findLayer(ver_layer)
262
+
263
+ H_WIDTH = int(Decimal(hor_width_mult) * H_LAYER.getWidth())
264
+ V_WIDTH = int(Decimal(ver_width_mult) * V_LAYER.getWidth())
265
+
266
+ if hor_length is not None:
267
+ H_LENGTH = int(micron_in_units * hor_length)
268
+ else:
269
+ H_LENGTH = max(
270
+ int(
271
+ math.ceil(
272
+ H_LAYER.getArea() * micron_in_units * micron_in_units / H_WIDTH
273
+ )
274
+ ),
275
+ H_WIDTH,
276
+ )
277
+
278
+ if ver_length is not None:
279
+ V_LENGTH = int(micron_in_units * ver_length)
280
+ else:
281
+ V_LENGTH = max(
282
+ int(
283
+ math.ceil(
284
+ V_LAYER.getArea() * micron_in_units * micron_in_units / V_WIDTH
285
+ )
286
+ ),
287
+ V_WIDTH,
288
+ )
289
+
290
+ # read config + calculate minima
291
+ config_file_str = open(config_file_name, "r", encoding="utf8").read()
292
+
293
+ try:
294
+ info_by_side = ioplace_parser.parse(config_file_str)
295
+ except ValueError as e:
296
+ print(f"An exception occurred: {e}")
297
+ exit(os.EX_DATAERR)
298
+
299
+ print("Top-level design name:", reader.name)
300
+
301
+ bterms = [
302
+ bterm
303
+ for bterm in reader.block.getBTerms()
304
+ if bterm.getSigType() not in ["POWER", "GROUND"]
305
+ ]
306
+
307
+ for side, side_info in info_by_side.items():
308
+ min = (
309
+ (V_WIDTH + V_LAYER.getSpacing())
310
+ if side in ["N", "S"]
311
+ else (H_WIDTH + H_LAYER.getSpacing())
312
+ ) / reader.dbunits
313
+ if side_info.min_distance is None:
314
+ side_info.min_distance = min
315
+ if side_info.min_distance < min:
316
+ print(
317
+ f"[WARNING] Overriding minimum distance {side_info.min_distance} with {min} for pins on side {side} to avoid overlap.",
318
+ file=sys.stderr,
319
+ )
320
+ side_info.min_distance = min
321
+
322
+ # build a list of pins
323
+ pin_placement = {"N": [], "E": [], "W": [], "S": []}
324
+
325
+ regex_by_bterm = {}
326
+ unmatched_regexes = set()
327
+ for side, side_info in info_by_side.items():
328
+ for pin in side_info.pins:
329
+ if isinstance(pin, int): # Virtual pins
330
+ pin_placement[side].append(pin)
331
+ continue
332
+
333
+ anchored_regex = f"^{pin}$" # anchor
334
+ matched = False
335
+ collected = []
336
+ for bterm in bterms:
337
+ pin_name = bterm.getName()
338
+ if re.match(anchored_regex, pin_name) is None:
339
+ continue
340
+ if bterm in regex_by_bterm:
341
+ print(
342
+ f"[ERROR] Multiple regexes matched {pin_name}. Those are {regex_by_bterm[bterm]} and {pin}",
343
+ file=sys.stderr,
344
+ )
345
+ sys.exit(os.EX_DATAERR)
346
+ regex_by_bterm[bterm] = pin
347
+ collected.append(bterm)
348
+ matched = True
349
+ collected.sort(key=partial(sorter, order=side_info.sort_mode))
350
+ pin_placement[side] += collected
351
+ if not matched:
352
+ unmatched_regexes.add(pin)
353
+
354
+ # check for extra or missing pins
355
+ not_in_design = unmatched_regexes
356
+ not_in_config = set(
357
+ [bterm.getName() for bterm in bterms if bterm not in regex_by_bterm]
358
+ )
359
+ mismatches_found = False
360
+ for is_in, not_in, pins in [
361
+ ("config", "design", not_in_design),
362
+ ("design", "config", not_in_config),
363
+ ]:
364
+ for name in pins:
365
+ if (
366
+ is_in == "config"
367
+ and (unmatched_error in {"unmatched_cfg", "both"})
368
+ or is_in == "design"
369
+ and (unmatched_error in {"unmatched_design", "both"})
370
+ ):
371
+ mismatches_found = True
372
+ print(
373
+ f"[ERROR] {name} not found in {not_in} but found in {is_in}.",
374
+ file=sys.stderr,
375
+ )
376
+ else:
377
+ print(
378
+ f"[WARNING] {name} not found in {not_in} but found in {is_in}.",
379
+ file=sys.stderr,
380
+ )
381
+
382
+ if mismatches_found:
383
+ print("Critical mismatches found.")
384
+ exit(os.EX_DATAERR)
385
+
386
+ if len(not_in_config) > 0:
387
+ print("Assigning random sides to unmatched pins…")
388
+ for bterm in not_in_config:
389
+ random_side = random.choice(list(pin_placement.keys()))
390
+ pin_placement[random_side].append(bterm)
391
+
392
+ # generate slots
393
+ DIE_AREA = reader.block.getDieArea()
394
+ BLOCK_LL_X = DIE_AREA.xMin()
395
+ BLOCK_LL_Y = DIE_AREA.yMin()
396
+ BLOCK_UR_X = DIE_AREA.xMax()
397
+ BLOCK_UR_Y = DIE_AREA.yMax()
398
+
399
+ print("Block boundaries:", BLOCK_LL_X, BLOCK_LL_Y, BLOCK_UR_X, BLOCK_UR_Y)
400
+
401
+ origin, count, h_step = reader.block.findTrackGrid(H_LAYER).getGridPatternY(0)
402
+ print(f"Horizontal Tracks Origin: {origin}, Count: {count}, Step: {h_step}")
403
+ h_tracks = grid_to_tracks(origin, count, h_step)
404
+
405
+ origin, count, v_step = reader.block.findTrackGrid(V_LAYER).getGridPatternX(0)
406
+ print(f"Vertical Tracks Origin: {origin}, Count: {count}, Step: {v_step}")
407
+ v_tracks = grid_to_tracks(origin, count, v_step)
408
+
409
+ pin_tracks = {}
410
+ for side in pin_placement:
411
+ if side in ["N", "S"]:
412
+ min_distance = info_by_side[side].min_distance * micron_in_units
413
+ pin_tracks[side] = [
414
+ v_tracks[i]
415
+ for i in range(len(v_tracks))
416
+ if (i % (math.ceil(min_distance / v_step))) == 0
417
+ ]
418
+ elif side in ["W", "E"]:
419
+ pin_tracks[side] = [
420
+ h_tracks[i]
421
+ for i in range(len(h_tracks))
422
+ if (
423
+ i
424
+ % (
425
+ math.ceil(
426
+ info_by_side[side].min_distance * micron_in_units / h_step
427
+ )
428
+ )
429
+ )
430
+ == 0
431
+ ]
432
+
433
+ # reversals (including randomly-assigned pins, if needed)
434
+ for side, side_info in info_by_side.items():
435
+ if side_info.reverse_result:
436
+ pin_placement[side].reverse()
437
+
438
+ # create the pins
439
+ for side in pin_placement:
440
+ slots, pin_placement[side] = equally_spaced_sequence(
441
+ side, pin_placement[side], pin_tracks[side]
442
+ )
443
+
444
+ assert len(slots) == len(pin_placement[side])
445
+
446
+ for i in range(len(pin_placement[side])):
447
+ bterm = pin_placement[side][i]
448
+ slot = slots[i]
449
+ pin_name = bterm.getName()
450
+ pins = bterm.getBPins()
451
+ if len(pins) > 0:
452
+ print(
453
+ f"[WARNING] {pin_name} already has shapes. The shapes will be modified.",
454
+ file=sys.stderr,
455
+ )
456
+ assert len(pins) == 1
457
+ pin_bpin = pins[0]
458
+ else:
459
+ pin_bpin = odb.dbBPin_create(bterm)
460
+
461
+ pin_bpin.setPlacementStatus("PLACED")
462
+
463
+ if side in ["N", "S"]:
464
+ rect = odb.Rect(0, 0, V_WIDTH, V_LENGTH + V_EXTENSION)
465
+ if side == "N":
466
+ y = BLOCK_UR_Y - V_LENGTH
467
+ else:
468
+ y = BLOCK_LL_Y - V_EXTENSION
469
+ rect.moveTo(slot - V_WIDTH // 2, y)
470
+ odb.dbBox_create(pin_bpin, V_LAYER, *rect.ll(), *rect.ur())
471
+ else:
472
+ rect = odb.Rect(0, 0, H_LENGTH + H_EXTENSION, H_WIDTH)
473
+ if side == "E":
474
+ x = BLOCK_UR_X - H_LENGTH
475
+ else:
476
+ x = BLOCK_LL_X - H_EXTENSION
477
+ rect.moveTo(x, slot - H_WIDTH // 2)
478
+ odb.dbBox_create(pin_bpin, H_LAYER, *rect.ll(), *rect.ur())
479
+
480
+
481
+ if __name__ == "__main__":
482
+ io_place()
@@ -0,0 +1,23 @@
1
+ # Copyright 2025 LibreLane Contributors
2
+ #
3
+ # Adapted from ioplace_parser
4
+ #
5
+ # Copyright 2020-2023 Efabless Corporation
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ """
19
+ This is a modified version of https://github.com/efabless/ioplace_parser that
20
+ does NOT use Antlr4 and instead uses a custom parser that is faster and
21
+ dependency-free.
22
+ """
23
+ from .parse import Side, Order, parse
@@ -0,0 +1,147 @@
1
+ # Copyright 2025 LibreLane Contributors
2
+ #
3
+ # Adapted from ioplace_parser
4
+ #
5
+ # Copyright 2020-2023 Efabless Corporation
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ import re
19
+ from enum import IntEnum
20
+ from typing import Literal, Optional, Dict, List, Union
21
+ from decimal import Decimal
22
+ import warnings
23
+
24
+ from dataclasses import dataclass, field
25
+
26
+
27
+ class Order(IntEnum):
28
+ busMajor = 0
29
+ bitMajor = 1
30
+
31
+
32
+ @dataclass
33
+ class Side:
34
+ min_distance: Optional[Decimal] = None
35
+ reverse_result: bool = False
36
+ pins: List[Union[str, int]] = field(default_factory=list)
37
+ sort_mode: Optional[Order] = Order.busMajor
38
+
39
+
40
+ VALUE_ANNOTATIONS = ["min_distance"]
41
+ STANDALONE_ANNOTATIONS = [
42
+ "bus_major",
43
+ "bit_major",
44
+ ]
45
+
46
+
47
+ def parse(string: str) -> Dict[Literal["N", "E", "W", "S"], Side]:
48
+ """
49
+ Parses a pin configuration into a dictionary of the four cardinal sides.
50
+
51
+ :param string: The input configuration as a string (not a file path)
52
+ :returns: A dictionary where each cardinal direction points to a Side object.
53
+ :raises ValueError: On syntax or token recognition errors
54
+ """
55
+ sides = {}
56
+ current_side: Optional[Side] = None
57
+ global_sort_mode: Order = Order.busMajor
58
+ global_min_distance: Optional[Decimal] = None
59
+
60
+ string_mut = string
61
+
62
+ ws_rx = re.compile(r"^\s+")
63
+ annotation_rx = re.compile(r"^@\s*(\w+)(?:\s*=\s*([0-9]+(?:.[0-9]+)?))?")
64
+ direction_rx = re.compile(r"^#\s*([NEWS]R?|BUS_SORT)")
65
+ virtual_pin_rx = re.compile(r"^\$\s*([0-9]+)")
66
+ non_ws_rx = re.compile(r"^\S+")
67
+ while len(string_mut):
68
+ # annotation
69
+ if skip_match := ws_rx.search(string_mut):
70
+ string_mut = string_mut[skip_match.end() :]
71
+ elif anno_match := annotation_rx.search(string_mut):
72
+ annotation = anno_match[1]
73
+ if annotation in VALUE_ANNOTATIONS:
74
+ if anno_match[2] is None:
75
+ raise ValueError(f"Annotation {annotation} requires a value")
76
+ value = anno_match[2]
77
+ if annotation == "min_distance":
78
+ if current_side is None:
79
+ global_min_distance = Decimal(value)
80
+ else:
81
+ current_side.min_distance = Decimal(value)
82
+ elif annotation in STANDALONE_ANNOTATIONS:
83
+ if anno_match[2] is not None:
84
+ raise ValueError(
85
+ f"Annotation {annotation} cannot be assigned a value"
86
+ )
87
+ if annotation == "bus_major":
88
+ if current_side is None:
89
+ global_sort_mode = Order.busMajor
90
+ else:
91
+ current_side.sort_mode = Order.busMajor
92
+ elif annotation == "bit_major":
93
+ if current_side is None:
94
+ global_sort_mode = Order.bitMajor
95
+ else:
96
+ current_side.sort_mode = Order.bitMajor
97
+ else:
98
+ raise ValueError(f"Unknown annotation '{annotation}'")
99
+ string_mut = string_mut[anno_match.end() :]
100
+ elif dir_match := direction_rx.search(string_mut):
101
+ direction = dir_match[1]
102
+ if direction == "BUS_SORT":
103
+ warnings.warn(
104
+ "Specifying bit-major using the direction token ('#BUS_SORT') is deprecated: use @bit_major."
105
+ )
106
+ global_sort_mode = Order.bitMajor
107
+ else:
108
+ current_side = Side(
109
+ min_distance=global_min_distance,
110
+ reverse_result=len(direction) == 2,
111
+ sort_mode=global_sort_mode,
112
+ )
113
+ side: Literal["N", "E", "W", "S"] = direction[0] # type: ignore
114
+ sides[side] = current_side
115
+ string_mut = string_mut[dir_match.end() :]
116
+ elif vp_match := virtual_pin_rx.search(string_mut):
117
+ count = int(vp_match[1])
118
+ if current_side is None:
119
+ raise ValueError(
120
+ f"virtual pin declaration ${count} requires a direction to be set first"
121
+ )
122
+ current_side.pins.append(count)
123
+ string_mut = string_mut[vp_match.end() :]
124
+ elif nonws_match := non_ws_rx.match(string_mut):
125
+ # assume regex
126
+ if current_side is None:
127
+ raise ValueError(
128
+ f"identifier/regex '{nonws_match[0]}' requires a direction to be set first"
129
+ )
130
+ current_side.pins.append(nonws_match[0])
131
+ string_mut = string_mut[nonws_match.end() :]
132
+ else:
133
+ raise ValueError(
134
+ f"Syntax Error: Unexpected character starting at {string_mut[:10]}…"
135
+ )
136
+
137
+ all_sides: List[Literal["N", "E", "W", "S"]] = ["N", "E", "W", "S"]
138
+ for side in all_sides:
139
+ if side in sides:
140
+ continue
141
+ sides[side] = Side(
142
+ min_distance=global_min_distance,
143
+ reverse_result=False,
144
+ sort_mode=global_sort_mode,
145
+ )
146
+
147
+ return sides