librelane 2.4.0.dev10__py3-none-any.whl → 2.4.0.dev12__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.

@@ -43,6 +43,7 @@ from .misc import (
43
43
  Filter,
44
44
  get_latest_file,
45
45
  process_list_file,
46
+ count_occurences,
46
47
  _get_process_limit,
47
48
  )
48
49
  from .types import (
librelane/common/drc.py CHANGED
@@ -208,6 +208,7 @@ class DRC:
208
208
  from lxml import etree as ET
209
209
 
210
210
  with ET.xmlfile(out, encoding="utf8", buffered=False) as xf:
211
+ xf.write_declaration()
211
212
  with xf.element("report-database"):
212
213
  # 1. Cells
213
214
  with xf.element("cells"):
librelane/common/misc.py CHANGED
@@ -11,6 +11,7 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
+ import io
14
15
  import os
15
16
  import re
16
17
  import glob
@@ -21,6 +22,7 @@ import pathlib
21
22
  import unicodedata
22
23
  from math import inf
23
24
  from typing import (
25
+ IO,
24
26
  Any,
25
27
  Generator,
26
28
  Iterable,
@@ -378,7 +380,7 @@ def _get_process_limit() -> int:
378
380
  return int(os.getenv("_OPENLANE_MAX_CORES", os.cpu_count() or 1))
379
381
 
380
382
 
381
- def gzopen(filename, mode="rt"):
383
+ def gzopen(filename: AnyPath, mode="rt") -> IO[Any]:
382
384
  """
383
385
  This method (tries to?) emulate the gzopen from the Linux Standard Base,
384
386
  specifically this part:
@@ -388,6 +390,11 @@ def gzopen(filename, mode="rt"):
388
390
  for reading directly from the file without any decompression.
389
391
 
390
392
  gzip.open does not have this behavior.
393
+
394
+ :param filename: The full path to the uncompressed or gzipped file.
395
+ :param mode: "r", "rb", "w", "wb", "x", "xb", "a" or "ab" for
396
+ binary mode, or "rt", "wt", "xt" or "at" for text mode.
397
+ :returns: An I/O wrapper that may very slightly based on the mode.
391
398
  """
392
399
  try:
393
400
  g = gzip.open(filename, mode=mode)
@@ -400,3 +407,18 @@ def gzopen(filename, mode="rt"):
400
407
  except gzip.BadGzipFile:
401
408
  g.close()
402
409
  return open(filename, mode=mode)
410
+
411
+
412
+ def count_occurences(fp: io.TextIOWrapper, pattern: str = "") -> int:
413
+ """
414
+ Counts the occurences of a certain string in a stream, line-by-line, without
415
+ necessarily loading the entire file into memory.
416
+
417
+ Equivalent to: ``grep -c 'pattern' <file>`` (but without regex support).
418
+
419
+ :param fp: the text stream
420
+ :param pattern: the substring to search for. if set to "", it will simply
421
+ count the lines in the file.
422
+ :returns: the number of matching lines
423
+ """
424
+ return sum(pattern in line for line in fp)
librelane/common/types.py CHANGED
@@ -16,6 +16,7 @@ import sys
16
16
  import tempfile
17
17
  from math import isfinite
18
18
  from decimal import Decimal
19
+ from weakref import finalize
19
20
  from collections import UserString
20
21
  from typing import Any, Union, ClassVar, Tuple, Optional
21
22
 
@@ -112,6 +113,4 @@ class ScopedFile(Path):
112
113
  super().__init__(self._ntf.name)
113
114
  self._ntf.write(contents)
114
115
  self._ntf.close()
115
-
116
- def __del__(self):
117
- os.unlink(self._ntf.name)
116
+ self._ntf_cleanup = finalize(self, os.unlink, self._ntf.name)
@@ -14,7 +14,6 @@
14
14
  import os
15
15
  import sys
16
16
  import json
17
- import functools
18
17
  from decimal import Decimal
19
18
 
20
19
  import click
@@ -111,9 +110,7 @@ def create_config(
111
110
  print("At least one source RTL file is required.", file=sys.stderr)
112
111
  exit(1)
113
112
  source_rtl_key = "VERILOG_FILES"
114
- if not functools.reduce(
115
- lambda acc, x: acc and (x.endswith(".sv") or x.endswith(".v")), source_rtl, True
116
- ):
113
+ if not all(file.endswith(".sv") or file.endswith(".v") for file in source_rtl):
117
114
  print(
118
115
  "Only Verilog/SystemVerilog files are supported by create-config.",
119
116
  file=sys.stderr,
librelane/flows/flow.py CHANGED
@@ -375,7 +375,7 @@ class Flow(ABC):
375
375
  self.progress_bar = FlowProgressBar(self.name)
376
376
 
377
377
  @classmethod
378
- def get_help_md(Self) -> str: # pragma: no cover
378
+ def get_help_md(Self, myst_anchors: bool = True) -> str: # pragma: no cover
379
379
  """
380
380
  :returns: rendered Markdown help for this Flow
381
381
  """
@@ -383,10 +383,12 @@ class Flow(ABC):
383
383
  if Self.__doc__:
384
384
  doc_string = textwrap.dedent(Self.__doc__)
385
385
 
386
+ flow_anchor = f"(flow-{slugify(Self.__name__, lower=True)})="
387
+
386
388
  result = (
387
389
  textwrap.dedent(
388
390
  f"""\
389
- (flow-{slugify(Self.__name__, lower=True)})=
391
+ {flow_anchor * myst_anchors}
390
392
  ### {Self.__name__}
391
393
 
392
394
  ```{{eval-rst}}
@@ -426,7 +428,8 @@ class Flow(ABC):
426
428
  for var in flow_config_vars:
427
429
  units = var.units or ""
428
430
  pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
429
- result += f"| `{var.name}`{{#{var._get_docs_identifier(Self.__name__)}}}{pdk_superscript} | {var.type_repr_md()} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
431
+ var_anchor = f"{{#{var._get_docs_identifier(Self.__name__)}}}"
432
+ result += f"| `{var.name}`{var_anchor * myst_anchors} {pdk_superscript} | {var.type_repr_md()} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
430
433
  result += "\n"
431
434
 
432
435
  if len(Self.Steps):
@@ -438,10 +441,35 @@ class Flow(ABC):
438
441
  name = step.name
439
442
  else:
440
443
  name = step.id
441
- result += f"* [`{step.id}`](./step_config_vars.md#{slugify(name)})\n"
444
+ if myst_anchors:
445
+ result += (
446
+ f"* [`{step.id}`](./step_config_vars.md#{slugify(name)})\n"
447
+ )
448
+ else:
449
+ result += f"* {step.id}"
442
450
 
443
451
  return result
444
452
 
453
+ @classmethod
454
+ def display_help(Self): # pragma: no cover
455
+ """
456
+ Displays Markdown help for a given flow.
457
+
458
+ If in an IPython environment, it's rendered using ``IPython.display``.
459
+ Otherwise, it's rendered using ``rich.markdown``.
460
+ """
461
+ try:
462
+ get_ipython() # type: ignore
463
+
464
+ import IPython.display
465
+
466
+ IPython.display.display(IPython.display.Markdown(Self.get_help_md()))
467
+ except NameError:
468
+ from ..logging import console
469
+ from rich.markdown import Markdown
470
+
471
+ console.log(Markdown(Self.get_help_md()))
472
+
445
473
  def get_all_config_variables(self) -> List[Variable]:
446
474
  """
447
475
  :returns: All configuration variables for this Flow, including
@@ -796,7 +824,6 @@ class Flow(ABC):
796
824
  DesignFormat.POWERED_NETLIST: (os.path.join("verilog", "gl"), "v"),
797
825
  DesignFormat.DEF: ("def", "def"),
798
826
  DesignFormat.LEF: ("lef", "lef"),
799
- DesignFormat.SDF: (os.path.join("sdf", "multicorner"), "sdf"),
800
827
  DesignFormat.SPEF: (os.path.join("spef", "multicorner"), "spef"),
801
828
  DesignFormat.LIB: (os.path.join("lib", "multicorner"), "lib"),
802
829
  DesignFormat.GDS: ("gds", "gds"),
@@ -855,38 +882,67 @@ class Flow(ABC):
855
882
  file_path, os.path.join(to_dir, file), follow_symlinks=True
856
883
  )
857
884
 
858
- signoff_folder = os.path.join(
859
- path, "signoff", self.config["DESIGN_NAME"], "librelane-signoff"
860
- )
861
- mkdirp(signoff_folder)
885
+ def find_one(pattern):
886
+ result = glob.glob(pattern)
887
+ if len(result) == 0:
888
+ return None
889
+ return result[0]
890
+
891
+ signoff_dir = os.path.join(path, "signoff", self.config["DESIGN_NAME"])
892
+ openlane_signoff_dir = os.path.join(signoff_dir, "openlane-signoff")
893
+ mkdirp(openlane_signoff_dir)
862
894
 
863
- # resolved.json
895
+ ## resolved.json
864
896
  shutil.copyfile(
865
897
  self.config_resolved_path,
866
- os.path.join(signoff_folder, "resolved.json"),
898
+ os.path.join(openlane_signoff_dir, "resolved.json"),
867
899
  follow_symlinks=True,
868
900
  )
869
901
 
870
- # Logs
871
- mkdirp(signoff_folder)
872
- copy_dir_contents(self.run_dir, signoff_folder, "*.log")
902
+ ## metrics
903
+ with open(os.path.join(signoff_dir, "metrics.csv"), "w", encoding="utf8") as f:
904
+ last_state.metrics_to_csv(f)
873
905
 
874
- # Step-specific
906
+ ## flow logs
907
+ mkdirp(openlane_signoff_dir)
908
+ copy_dir_contents(self.run_dir, openlane_signoff_dir, "*.log")
909
+
910
+ ### step-specific signoff logs and reports
875
911
  for step in self.step_objects:
876
912
  reports_dir = os.path.join(step.step_dir, "reports")
877
913
  step_imp_id = step.get_implementation_id()
914
+ if step_imp_id == "Magic.DRC":
915
+ if drc_rpt := find_one(os.path.join(reports_dir, "*.rpt")):
916
+ shutil.copyfile(
917
+ drc_rpt, os.path.join(openlane_signoff_dir, "drc.rpt")
918
+ )
919
+ if drc_xml := find_one(os.path.join(reports_dir, "*.xml")):
920
+ # Despite the name, this is the Magic DRC report simply
921
+ # converted into a KLayout-compatible format. Confusing!
922
+ drc_xml_out = os.path.join(openlane_signoff_dir, "drc.klayout.xml")
923
+ with open(drc_xml, encoding="utf8") as i, open(
924
+ drc_xml_out, "w", encoding="utf8"
925
+ ) as o:
926
+ o.write(
927
+ "<!-- Despite the name, this is the Magic DRC report in KLayout format. -->\n"
928
+ )
929
+ shutil.copyfileobj(i, o)
930
+ if step_imp_id == "Netgen.LVS":
931
+ if lvs_rpt := find_one(os.path.join(reports_dir, "*.rpt")):
932
+ shutil.copyfile(
933
+ lvs_rpt, os.path.join(openlane_signoff_dir, "lvs.rpt")
934
+ )
878
935
  if step_imp_id.endswith("DRC") or step_imp_id.endswith("LVS"):
879
- if os.path.exists(reports_dir):
880
- copy_dir_contents(reports_dir, signoff_folder)
881
- if step_imp_id.endswith("LVS"):
882
- copy_dir_contents(step.step_dir, signoff_folder, "*.log")
936
+ copy_dir_contents(step.step_dir, openlane_signoff_dir, "*.log")
883
937
  if step_imp_id.endswith("CheckAntennas"):
884
938
  if os.path.exists(reports_dir):
885
939
  copy_dir_contents(
886
- reports_dir, signoff_folder, "antenna_summary.rpt"
940
+ reports_dir, openlane_signoff_dir, "antenna_summary.rpt"
887
941
  )
888
942
  if step_imp_id.endswith("STAPostPNR"):
889
- timing_report_folder = os.path.join(signoff_folder, "timing-reports")
943
+ timing_report_folder = os.path.join(
944
+ openlane_signoff_dir, "timing-reports"
945
+ )
890
946
  mkdirp(timing_report_folder)
891
947
  copy_dir_contents(step.step_dir, timing_report_folder, "*summary.rpt")
892
948
  for dir in os.listdir(step.step_dir):
@@ -897,6 +953,18 @@ class Flow(ABC):
897
953
  mkdirp(target)
898
954
  copy_dir_contents(dir_path, target, "*.rpt")
899
955
 
956
+ # 3. SDF
957
+ # (This one, as with many things in the Efabless format, is special)
958
+ if sdf := last_state[DesignFormat.SDF]:
959
+ assert isinstance(sdf, dict), "SDF is not a dictionary"
960
+ for corner, view in sdf.items():
961
+ assert isinstance(view, Path), "SDF state out returned multiple paths"
962
+ target_dir = os.path.join(signoff_dir, "sdf", corner)
963
+ mkdirp(target_dir)
964
+ shutil.copyfile(
965
+ view, os.path.join(target_dir, f"{self.config['DESIGN_NAME']}.sdf")
966
+ )
967
+
900
968
  @deprecated(
901
969
  version="2.0.0a46",
902
970
  reason="Use .progress_bar.set_max_stage_count",
@@ -77,5 +77,3 @@ if { $::env(MAGIC_GDS_POLYGON_SUBCELLS) } {
77
77
 
78
78
  gds write $::env(SAVE_MAG_GDS)
79
79
  puts "\[INFO\] GDS Write Complete"
80
-
81
- exit 0
@@ -76,4 +76,3 @@ puts stdout "\[INFO\] Saving mag view with DRC errors ($mag_view)"
76
76
  # WARNING: changes the name of the cell; keep as last step
77
77
  save $mag_view
78
78
  puts stdout "\[INFO\] Saved"
79
- exit 0
@@ -43,5 +43,3 @@ if { [info exist ::env(EXTRA_GDS_FILES)] } {
43
43
  puts "\[INFO\] Saved mag view from $gds_file under $::env(STEP_DIR)"
44
44
  }
45
45
  }
46
-
47
- exit 0
@@ -29,4 +29,3 @@ set final_filepath $::env(signoff_tmpfiles)/gds_ptrs.mag
29
29
  file rename -force $::env(signoff_tmpfiles)/$::env(DESIGN_NAME).mag $final_filepath
30
30
 
31
31
  puts "\[INFO\] Wrote $final_filepath including GDS pointers."
32
- exit 0
@@ -59,5 +59,3 @@ foreach design_name [cellname list allcells] {
59
59
  puts $fp [join $new_mag_lines "\n"]
60
60
  close $fp
61
61
  }
62
-
63
- exit 0
@@ -24,4 +24,3 @@ cellname filepath $::env(DESIGN_NAME).lef $::env(signoff_results)
24
24
  save
25
25
 
26
26
  puts "\[INFO\] DONE GENERATING MAGLEF VIEW"
27
- exit 0
@@ -17,3 +17,5 @@ if {[catch {source $::env(_MAGIC_SCRIPT)} err]} {
17
17
  puts "Error: $err"
18
18
  exit 1
19
19
  }
20
+
21
+ exit 0
@@ -17,7 +17,6 @@ import utl
17
17
 
18
18
  import re
19
19
  import json
20
- import functools
21
20
  from dataclasses import dataclass
22
21
  from typing import Dict, List, Optional
23
22
 
@@ -49,11 +48,14 @@ class Design(object):
49
48
  def get_verilog_net_name_by_bit(self, top_module: str, target_bit: int):
50
49
  yosys_design_object = self.yosys_dict["modules"][top_module]
51
50
  if top_module not in self.verilog_net_names_by_bit_by_module:
52
- self.verilog_net_names_by_bit_by_module[top_module] = functools.reduce(
53
- lambda a, b: {**a, **{bit: b[0] for bit in b[1]["bits"]}},
54
- yosys_design_object["netnames"].items(),
55
- {},
56
- )
51
+ # check git history for a version of this loop that is drunk on power
52
+ netname_by_bit = {}
53
+
54
+ for netname, info in yosys_design_object["netnames"].items():
55
+ for bit in info["bits"]:
56
+ netname_by_bit[bit] = netname
57
+
58
+ self.verilog_net_names_by_bit_by_module[top_module] = netname_by_bit
57
59
  return self.verilog_net_names_by_bit_by_module[top_module][target_bit]
58
60
 
59
61
  def get_pins(self, module_name: str) -> Dict[str, odb.dbMTerm]:
@@ -20,7 +20,7 @@ import sys
20
20
  import json
21
21
  import locale
22
22
  import inspect
23
- import functools
23
+ from functools import wraps
24
24
  from decimal import Decimal
25
25
  from fnmatch import fnmatch
26
26
  from typing import Callable, Dict
@@ -188,7 +188,7 @@ class OdbReader(object):
188
188
 
189
189
 
190
190
  def click_odb(function):
191
- @functools.wraps(function)
191
+ @wraps(function)
192
192
  def wrapper(input_db, input_lefs, config_path, **kwargs):
193
193
  reader = OdbReader(input_db, config_path=config_path)
194
194
 
librelane/state/state.py CHANGED
@@ -13,7 +13,9 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ import io
16
17
  import os
18
+ import csv
17
19
  import sys
18
20
  import json
19
21
  import shutil
@@ -214,14 +216,20 @@ class State(GenericImmutableDict[str, StateElement]):
214
216
  self._walk(self, path, visitor)
215
217
  metrics_csv_path = os.path.join(path, "metrics.csv")
216
218
  with open(metrics_csv_path, "w", encoding="utf8") as f:
217
- f.write("Metric,Value\n")
218
- for metric in self.metrics:
219
- f.write(f"{metric},{self.metrics[metric]}\n")
219
+ self.metrics_to_csv(f)
220
220
 
221
221
  metrics_json_path = os.path.join(path, "metrics.json")
222
222
  with open(metrics_json_path, "w", encoding="utf8") as f:
223
223
  f.write(self.metrics.dumps())
224
224
 
225
+ def metrics_to_csv(
226
+ self, fp: io.TextIOWrapper, metrics_object: Optional[Dict[str, Any]] = None
227
+ ):
228
+ w = csv.writer(fp)
229
+ w.writerow(("Metric", "Value"))
230
+ for entry in (metrics_object or self.metrics).items():
231
+ w.writerow(entry)
232
+
225
233
  def validate(self):
226
234
  """
227
235
  Ensures that all paths exist in a State.
@@ -15,7 +15,6 @@ import os
15
15
  import shlex
16
16
  import shutil
17
17
  import datetime
18
- import functools
19
18
  import subprocess
20
19
  from functools import partial
21
20
  from typing import IO, Any, Dict, Optional, Sequence, Union
@@ -239,7 +238,7 @@ def eject(ctx, output, state_in, config, id):
239
238
  found_stdin_data = found_stdin.read()
240
239
  raise Stop()
241
240
 
242
- step.run_subprocess = functools.partial(
241
+ step.run_subprocess = partial(
243
242
  step.run_subprocess,
244
243
  _popen_callable=popen_substitute,
245
244
  )
librelane/steps/magic.py CHANGED
@@ -14,7 +14,6 @@
14
14
  import os
15
15
  import re
16
16
  import shutil
17
- import functools
18
17
  import subprocess
19
18
  from signal import SIGKILL
20
19
  from decimal import Decimal
@@ -34,7 +33,8 @@ from .tclstep import TclStep
34
33
  from ..state import DesignFormat, State
35
34
 
36
35
  from ..config import Variable
37
- from ..common import get_script_dir, DRC as DRCObject, Path, mkdirp
36
+ from ..common import get_script_dir, DRC as DRCObject, Path, mkdirp, count_occurences
37
+ from ..logging import warn
38
38
 
39
39
 
40
40
  class MagicOutputProcessor(OutputProcessor):
@@ -464,6 +464,12 @@ class SpiceExtraction(MagicStep):
464
464
  "Extracts a SPICE netlist based on black-boxed standard cells and macros (basically, anything with a LEF) rather than transistors. An error will be thrown if both this and `MAGIC_EXT_USE_GDS` is set to ``True``.",
465
465
  default=False,
466
466
  ),
467
+ Variable(
468
+ "MAGIC_FEEDBACK_CONVERSION_THRESHOLD",
469
+ int,
470
+ "If Magic provides more feedback items than this threshold, conversion to KLayout databases is skipped (as something has gone horribly wrong.)",
471
+ default=10000,
472
+ ),
467
473
  ]
468
474
 
469
475
  def get_script_path(self):
@@ -481,22 +487,29 @@ class SpiceExtraction(MagicStep):
481
487
 
482
488
  views_updates, metrics_updates = super().run(state_in, env=env, **kwargs)
483
489
 
484
- cif_scale = Decimal(open(os.path.join(self.step_dir, "cif_scale.txt")).read())
485
490
  feedback_path = os.path.join(self.step_dir, "feedback.txt")
491
+ with open(feedback_path, encoding="utf8") as f:
492
+ illegal_overlap_count = count_occurences(f, "Illegal overlap")
493
+
494
+ metrics_updates["magic__illegal_overlap__count"] = illegal_overlap_count
495
+ threshold = self.config["MAGIC_FEEDBACK_CONVERSION_THRESHOLD"]
496
+ if illegal_overlap_count > threshold:
497
+ warn(
498
+ f"Not converting the feedback to the KLayout database format: {illegal_overlap_count} > MAGIC_FEEDBACK_CONVERSION_THRESHOLD ({threshold}). You may manually increase the threshold, but it might take forever."
499
+ )
500
+ return views_updates, metrics_updates
501
+
502
+ cif_scale = Decimal(open(os.path.join(self.step_dir, "cif_scale.txt")).read())
486
503
  try:
487
504
  se_feedback, _ = DRCObject.from_magic_feedback(
488
505
  open(feedback_path, encoding="utf8"),
489
506
  cif_scale,
490
507
  self.config["DESIGN_NAME"],
491
508
  )
492
- illegal_overlap_count = functools.reduce(
493
- lambda a, b: a + len(b.bounding_boxes),
494
- [
495
- v
496
- for v in se_feedback.violations.values()
497
- if "Illegal overlap" in v.description
498
- ],
499
- 0,
509
+ illegal_overlap_count = sum(
510
+ len(v.bounding_boxes)
511
+ for v in se_feedback.violations.values()
512
+ if "Illegal overlap" in v.description
500
513
  )
501
514
  with open(os.path.join(self.step_dir, "feedback.xml"), "wb") as f:
502
515
  se_feedback.to_klayout_xml(f)
@@ -505,9 +518,6 @@ class SpiceExtraction(MagicStep):
505
518
  self.warn(
506
519
  f"Failed to convert SPICE extraction feedback to KLayout database format: {e}"
507
520
  )
508
- metrics_updates["magic__illegal_overlap__count"] = (
509
- open(feedback_path, encoding="utf8").read().count("Illegal overlap")
510
- )
511
521
  return views_updates, metrics_updates
512
522
 
513
523
 
librelane/steps/odb.py CHANGED
@@ -17,7 +17,6 @@ import json
17
17
  import shutil
18
18
  from math import inf
19
19
  from decimal import Decimal
20
- from functools import reduce
21
20
  from abc import abstractmethod
22
21
  from dataclasses import dataclass
23
22
  from typing import Dict, List, Literal, Optional, Tuple
@@ -414,9 +413,7 @@ class ManualMacroPlacement(OdbpyStep):
414
413
  )
415
414
  shutil.copyfile(cfg_ref, cfg_file)
416
415
  elif macros := self.config.get("MACROS"):
417
- instance_count = reduce(
418
- lambda x, y: x + len(y.instances), macros.values(), 0
419
- )
416
+ instance_count = sum(len(m.instances) for m in macros.values())
420
417
  if instance_count >= 1:
421
418
  with open(cfg_file, "w") as f:
422
419
  for module, macro in macros.items():
@@ -19,7 +19,6 @@ import io
19
19
  import os
20
20
  import re
21
21
  import json
22
- import functools
23
22
  import subprocess
24
23
  from enum import Enum
25
24
  from math import inf
@@ -1147,9 +1146,7 @@ def get_psm_error_count(rpt: io.TextIOWrapper) -> int:
1147
1146
 
1148
1147
  sio.seek(0)
1149
1148
  violations = yaml.load(sio, Loader=yaml.SafeLoader) or []
1150
- return functools.reduce(
1151
- lambda acc, current: acc + len(current["srcs"]), violations, 0
1152
- )
1149
+ return sum(len(violation["srcs"]) for violation in violations)
1153
1150
 
1154
1151
 
1155
1152
  @Step.factory.register()
librelane/steps/step.py CHANGED
@@ -625,6 +625,7 @@ class Step(ABC):
625
625
  *,
626
626
  docstring_override: str = "",
627
627
  use_dropdown: bool = False,
628
+ myst_anchors: bool = False,
628
629
  ): # pragma: no cover
629
630
  """
630
631
  Renders Markdown help for this step to a string.
@@ -682,11 +683,12 @@ class Step(ABC):
682
683
  result += f"| {input_str} | {output_str} |\n"
683
684
 
684
685
  if len(Self.config_vars):
686
+ all_vars_anchor = f"({Self.id.lower()}-configuration-variables)="
685
687
  result += textwrap.dedent(
686
688
  f"""
687
- ({Self.id.lower()}-configuration-variables)=
689
+ {all_vars_anchor * myst_anchors}
688
690
  #### Configuration Variables
689
-
691
+
690
692
  | Variable Name | Type | Description | Default | Units |
691
693
  | - | - | - | - | - |
692
694
  """
@@ -694,13 +696,15 @@ class Step(ABC):
694
696
  for var in Self.config_vars:
695
697
  units = var.units or ""
696
698
  pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
697
- result += f"| `{var.name}`{{#{var._get_docs_identifier(Self.id)}}}{pdk_superscript} | {var.type_repr_md(for_document=True)} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
699
+ var_anchor = f"{{#{var._get_docs_identifier(Self.id)}}}"
700
+ result += f"| `{var.name}`{var_anchor * myst_anchors} {pdk_superscript} | {var.type_repr_md(for_document=True)} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
698
701
  result += "\n"
699
702
 
703
+ step_anchor = f"(step-{slugify(Self.id.lower())})="
700
704
  result = (
701
705
  textwrap.dedent(
702
706
  f"""
703
- (step-{slugify(Self.id.lower())})=
707
+ {step_anchor * myst_anchors}
704
708
  ### {Self.__get_desc()}
705
709
  """
706
710
  )
@@ -712,11 +716,22 @@ class Step(ABC):
712
716
  @classmethod
713
717
  def display_help(Self): # pragma: no cover
714
718
  """
715
- IPython-only. Displays Markdown help for a given step.
719
+ Displays Markdown help for this Step.
720
+
721
+ If in an IPython environment, it's rendered using ``IPython.display``.
722
+ Otherwise, it's rendered using ``rich.markdown``.
716
723
  """
717
- import IPython.display
724
+ try:
725
+ get_ipython() # type: ignore
726
+
727
+ import IPython.display
728
+
729
+ IPython.display.display(IPython.display.Markdown(Self.get_help_md()))
730
+ except NameError:
731
+ from ..logging import console
732
+ from rich.markdown import Markdown
718
733
 
719
- IPython.display.display(IPython.display.Markdown(Self.get_help_md()))
734
+ console.log(Markdown(Self.get_help_md()))
720
735
 
721
736
  def _repr_markdown_(self) -> str: # pragma: no cover
722
737
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: librelane
3
- Version: 2.4.0.dev10
3
+ Version: 2.4.0.dev12
4
4
  Summary: An infrastructure for implementing chip design flows
5
5
  Home-page: https://github.com/librelane/librelane
6
6
  License: Apache-2.0
@@ -1,23 +1,23 @@
1
1
  librelane/__init__.py,sha256=EMpoZrRmS_wsweKjhyAg52OXCK7HWQ8o8CVrYaX4ub0,1220
2
2
  librelane/__main__.py,sha256=GTPX4SM4euBFFFOd3ah_il5GEBBBOa6af6QC4KH2bjE,14792
3
3
  librelane/__version__.py,sha256=dbE4stCACDmIoxgKksesAkTa-_hi5dW6nPLWw9Pfq3Q,1486
4
- librelane/common/__init__.py,sha256=LrzxjZKJu3-i8oEYdXtV1IxkXicHtoSUNRcOGrGVGsw,1516
4
+ librelane/common/__init__.py,sha256=-wJi_dA7MtK25LC-wgHl1QUP7UVoDzv3uIo2bO1bPLQ,1538
5
5
  librelane/common/cli.py,sha256=xi48GBGHRsYrLGwx40ARwpykHx7GnuHbJjHxjOwtZ5Y,2349
6
- librelane/common/drc.py,sha256=2N5jPbM0cxnOj2Kci2AhHTTgHcynTtkV118ii3kq7IU,9603
6
+ librelane/common/drc.py,sha256=3AUxg986X_lCPrQvDkLekVTeC4-4qS7ZDpVMKlVkEmo,9638
7
7
  librelane/common/generic_dict.py,sha256=ASa5wtVcLuGlsBqGfLxLYXrYksqQB62iHljV04plIqI,10010
8
8
  librelane/common/metrics/__init__.py,sha256=nzdmeia1fN4CDPT604v-OHaBVydz-M4hz232g-jxJ6M,1521
9
9
  librelane/common/metrics/__main__.py,sha256=VfoOHFkGXtei6GLkPUD42X6io8QKFCHhxLMWOXPz_5E,12473
10
10
  librelane/common/metrics/library.py,sha256=CG7rubLdjuCQL9-9bzAC-64hf-KlH-iu_Fg0oKuesqs,7373
11
11
  librelane/common/metrics/metric.py,sha256=h3Xd26z5M80IJgVmmrBTjKcdGLb4I0wyjM-H4jdyi_0,6990
12
12
  librelane/common/metrics/util.py,sha256=Bl_9znlot7-Os2VigYLSmMf56aAkGdv3evWz9vfK7K4,9344
13
- librelane/common/misc.py,sha256=gxf5kDwn6m1Zib9BWK19nfAb2HLEBVlH-OyR3MiGpFQ,12408
13
+ librelane/common/misc.py,sha256=2APN-tzLGSmpecQbpPsmXEL7t1H49CcAmvem_x8MbeM,13257
14
14
  librelane/common/ring_buffer.py,sha256=DGFen9y0JOmiL7E27tmzDTVSJKZtV-waF9hl5Rz9uek,1898
15
15
  librelane/common/tcl.py,sha256=G0__oR6azVCS2Aue87zboba7vAR7v-SVQeUbqfviiDA,2709
16
16
  librelane/common/toolbox.py,sha256=fBMkpoOOL7rdbwi3W3-U5rrCxSplAqFZtlTcJtjLgMY,20968
17
17
  librelane/common/tpe.py,sha256=Txj0fVscXSDJTYmEKZ2ESFHOeqrhHnaPPiwWBgyx4g8,1285
18
- librelane/common/types.py,sha256=oclAQkeluz_iopI_28clHzxvac7gN5moT8Rzipy5mgM,3468
18
+ librelane/common/types.py,sha256=xo_OKq-2ue7JVpyQb6oUu6JuVSnLNEFKQCPBqNhZnQ4,3509
19
19
  librelane/config/__init__.py,sha256=lbJmD5CbrrrnaNdIUWqFIK488ea0uyej3iExh-9mkgE,1107
20
- librelane/config/__main__.py,sha256=6KSXxM4qNE2yJhizUsF1kdMsY1kY7hLHoPoz50POsS8,4532
20
+ librelane/config/__main__.py,sha256=NsJGoIOb950mdXql1zmzSq10wuFovK9NGWm011NNJ3A,4474
21
21
  librelane/config/config.py,sha256=WUznKnVYLn7ZNbUL4YMkMX7akmyc2S26ksQSicKeN1c,34964
22
22
  librelane/config/flow.py,sha256=qCGaUOj12j57gORzoE10m7_WG-n600llnFDMlZagUF4,16660
23
23
  librelane/config/pdk_compat.py,sha256=rznq5xIny9M0PmddhPOGtCIrSdv98ysAoYgkpyM0gUA,8450
@@ -43,7 +43,7 @@ librelane/flows/__init__.py,sha256=ghtmUG-taVpHJ3CKJRYZGn3dU0r93araT1EIGlBEsxg,8
43
43
  librelane/flows/builtins.py,sha256=tR14Qc1ZUey2w-Ar4DWOvxuP7LGPtMecCJq8WgcYJpk,773
44
44
  librelane/flows/classic.py,sha256=fI-LNhrvi7lzfsHRyJv_yjgFbpbWBVxN-9QpsgDxpTQ,10940
45
45
  librelane/flows/cli.py,sha256=P2LCFn5_RQ88yB0WuetpLAuWeKQXd-DrpCOMgnVh9Mg,16705
46
- librelane/flows/flow.py,sha256=wyYw-w6NIbCCfyfgwiq3BpztLlvZkRFUUeePoI9DpaU,34167
46
+ librelane/flows/flow.py,sha256=0dck9SsAlHulamvmFvxx2LPoRdQGJ9gUG9fiJ_6LAGc,37095
47
47
  librelane/flows/misc.py,sha256=32Om3isexesfKKiJZCajNmINc-xdv7eVx_tgoh9SR6U,2015
48
48
  librelane/flows/optimizing.py,sha256=OwZz6WGmXpliwO8vtmhjKHD-kzDyNv-zoCECZIigXsI,6076
49
49
  librelane/flows/sequential.py,sha256=DLzgvHKq0cO-U-eLO98zIFRnhGLfRv80_ozSX973TlI,13350
@@ -64,19 +64,19 @@ librelane/scripts/magic/Readme.md,sha256=NaQrlxY8l8GT-kokJNlMHeAR3PksWVbFpSznOWW
64
64
  librelane/scripts/magic/common/read.tcl,sha256=BcTGgul2TuSVd0TmhXrR0mvwE4hVO6NaHcy_5uLsOMI,3132
65
65
  librelane/scripts/magic/def/antenna_check.tcl,sha256=T_r1CWgySFVlLMcoTrNXQ_aMRs_fBAUYUUjyXJV1Sg0,1019
66
66
  librelane/scripts/magic/def/mag.tcl,sha256=PuL3MH6pmZP5Qh2cJ0GygWNzaYjdCSCoAbOli-JB4fs,707
67
- librelane/scripts/magic/def/mag_gds.tcl,sha256=pPNKntSkVM0fzHuyVrUtxWc7HWY2-MIMoId6PW8SIT4,2137
68
- librelane/scripts/magic/drc.tcl,sha256=slHVfI6QoApKc2S6V3MIqcV5xnKRMJ_1wSGleJzkHpk,2402
67
+ librelane/scripts/magic/def/mag_gds.tcl,sha256=zZESdGgKZG6__Z9f8Kjxhw3Fx4FMpYlSBGCilggIuv8,2129
68
+ librelane/scripts/magic/drc.tcl,sha256=gPGyI96lR10dJXcJACajzHaHiT6ayAYPJqrmmuQkABc,2395
69
69
  librelane/scripts/magic/extract_spice.tcl,sha256=lFNXfjIIzAWb2wda9ZtBOecENOOXyFBsh9HgrPYf7VQ,2737
70
70
  librelane/scripts/magic/gds/drc_batch.tcl,sha256=O76rwxSrQgoCuoxk36tRBZkQaeMfJknlHrQA3mtU2JU,2198
71
71
  librelane/scripts/magic/gds/erase_box.tcl,sha256=wsVSwMlkZFJa_MEGNsdXLnXFvjZlrl_lzIWkJjcDBgg,929
72
- librelane/scripts/magic/gds/extras_mag.tcl,sha256=aEABKbyUukMr3AhCCgpE8WpEzWwZzeFbqlQ8kAQJFYI,1325
73
- librelane/scripts/magic/gds/mag_with_pointers.tcl,sha256=mK0MEeQMRPN0MopgMlF2v5mq9jkssHEFRKv5gssWbLI,1063
72
+ librelane/scripts/magic/gds/extras_mag.tcl,sha256=hOmFK12wmGiA_H70YS0jVK4Faav-1sTCpajUxjTsopM,1317
73
+ librelane/scripts/magic/gds/mag_with_pointers.tcl,sha256=mfYTQdio1XAA0DpWlV5JS7c3rkxtSS06kZqVfDfKGuQ,1056
74
74
  librelane/scripts/magic/get_bbox.tcl,sha256=iC0D5Pw09ZiNxHyy70Z0_axlc7eIT_jVGmvlutM8z_4,377
75
- librelane/scripts/magic/lef/extras_maglef.tcl,sha256=PsLCHxLslS3UxspmYf_vniL4Jzq54nZTa2AB5F90i58,1594
76
- librelane/scripts/magic/lef/maglef.tcl,sha256=MyCuqqR9zhKaC7OAVU2iCFIMkSZxl4hyc5bsEVnBG2k,863
75
+ librelane/scripts/magic/lef/extras_maglef.tcl,sha256=V0TivPN4aLJ0is497IAmOB1qQm8vmCqqycKuwV3w0HY,1586
76
+ librelane/scripts/magic/lef/maglef.tcl,sha256=I6-xFtsCZQtQI4UbU53gaWrpELWUd4agNF5LLgq9Eq0,856
77
77
  librelane/scripts/magic/lef.tcl,sha256=Ij_v63siQrgYh56juCiZMcJBBCl2Lco3Ah4YKg-xiAU,2076
78
78
  librelane/scripts/magic/open.tcl,sha256=qgk3HiT4qI-pMWn2E2MxwijgveCVmt6oCd8wEWh0jio,915
79
- librelane/scripts/magic/wrapper.tcl,sha256=rBeBSExykheFx7MTejjnX_OKROnWeDYNE5ciCAQvueU,704
79
+ librelane/scripts/magic/wrapper.tcl,sha256=qO33N2AiEYBTABMB5vWMWWEd2FpyOhg8JlcGsBVD6MY,712
80
80
  librelane/scripts/netgen/setup.tcl,sha256=LBkdtVZpxrNGQ8UNwMkEQ-xSrkn9DNIBW_jzB9b1gYk,979
81
81
  librelane/scripts/odbpy/apply_def_template.py,sha256=Tn6y65biu0bAQ6XilYxq5jn3a_KqjTl423-aXWI864k,1534
82
82
  librelane/scripts/odbpy/cell_frequency.py,sha256=NfGgM8wxIvjM1C_GHUghZPOh8gpdasLOWR4qBdHHLFE,3105
@@ -94,9 +94,9 @@ librelane/scripts/odbpy/ioplace_parser/parse.py,sha256=L2GXzNA-gkjyySZcTWXrRRP8r
94
94
  librelane/scripts/odbpy/label_macro_pins.py,sha256=n3o9-_g6HkVP8k49yNnCkQJms9f_ykCE0Rye7bVFtIk,8620
95
95
  librelane/scripts/odbpy/lefutil.py,sha256=XhfWSGHdn96yZWYQAPisgJM0iuY3xw4SW7jmMTzbpZs,3064
96
96
  librelane/scripts/odbpy/placers.py,sha256=mgy_-GYeLDPMG41YAopMTtJyCHP6ucJRk7cJzI9PLRQ,4572
97
- librelane/scripts/odbpy/power_utils.py,sha256=al12uMiv8G0yQZOPKXNHYQ1dm2KGlu9xigSuYLEAo_A,14627
97
+ librelane/scripts/odbpy/power_utils.py,sha256=qbwhvW0QRiqtFXpYNGyfDIrhNZv9dt0JX2n4CQ8YRs8,14722
98
98
  librelane/scripts/odbpy/random_place.py,sha256=TEsV4LtXQTP8OJvnBh09Siu9fKkwG9UpIkCkQpdXAgU,1649
99
- librelane/scripts/odbpy/reader.py,sha256=5-hpG3TgmKwJtnbEDQoAPejeDx1QyRBRbIMULMkBesM,8539
99
+ librelane/scripts/odbpy/reader.py,sha256=omOCANavm7khGq3AvDyJX00IvhDn_15cW64IU-u2dRQ,8540
100
100
  librelane/scripts/odbpy/remove_buffers.py,sha256=f-kGZIPnMtu4gnl2r2CDkng8U8vUMJKJWNV_akOpc38,5460
101
101
  librelane/scripts/odbpy/snap_to_grid.py,sha256=lULRWlcYXvrTBUpemUPlpO2dBnbFeriuG-DlI4KnViE,1743
102
102
  librelane/scripts/odbpy/wire_lengths.py,sha256=pSPhVnLlvcvmgEh89G8nu8DRaZVP66r-4ieVoV3zrm4,2737
@@ -145,25 +145,25 @@ librelane/scripts/tclsh/hello.tcl,sha256=kkR3akY7QnGHYXsQODYwLkMkUEOgWcNFtzaMTTE
145
145
  librelane/state/__init__.py,sha256=rLUdAkeB278r8pB2Jpv-ccmmmP32FR90wANIFHXdA0w,969
146
146
  librelane/state/__main__.py,sha256=Ici4Ejg1ICUZNSYZRguC3BfEk_wFxsmE0ag0Vv8iY1I,1679
147
147
  librelane/state/design_format.py,sha256=ISl4O18ky1BiMkQn8Si0Tcf8UA5vrlZ1yay0iKPxvfk,5794
148
- librelane/state/state.py,sha256=J05gAeSVDiF76ITuw4WJZ7WkMyG4oTjt_7kpsI3E3PE,11957
148
+ librelane/state/state.py,sha256=-vg7pYJ7U7uzC26oDKM49H410fm2-kV03Hf0bg3dMus,12149
149
149
  librelane/steps/__init__.py,sha256=j3JYrdnWM74dYuEvE931oSrQI7FUz-hKWr8Mts8C0wg,1668
150
- librelane/steps/__main__.py,sha256=GviXtDLISKJCufKxK3oFPOSMF1GyShZbG5RXpVCYFkk,13376
150
+ librelane/steps/__main__.py,sha256=GQZiV4s-9GIF4AwP34W61zwgzMvPp-QTR4cNELi7r5c,13349
151
151
  librelane/steps/checker.py,sha256=vul1D0cT03144qKK5QAKswClKKICK7kNB4PB6xXykvc,21353
152
152
  librelane/steps/common_variables.py,sha256=qT0VeIstFsDbe8VGAyqXrXxQw69OZ08haM6u1IbdFiE,10429
153
153
  librelane/steps/cvc_rv.py,sha256=32vxFIbzSbrDtE0fXvdoQ-v3LVMrfi3r88f8Y-TKPKg,5531
154
154
  librelane/steps/klayout.py,sha256=g7jYz-1cLwgfPTiMJIdAQ9zmkrwNtJLPoRg6PqOUv6Y,16490
155
- librelane/steps/magic.py,sha256=4o_WarBAQdTTuekP72uovjvqW5wsaDCpMB3LtAhC_IY,20051
155
+ librelane/steps/magic.py,sha256=60Ko7sf_E3KNO18op_pyojrBdfej0v5NCexXxmI0X90,20716
156
156
  librelane/steps/misc.py,sha256=Xk_a6JJPljkk8pemu-NtlzDRs-8S7vuRKZKj4pnCRlE,5690
157
157
  librelane/steps/netgen.py,sha256=R9sDWv-9wKMdi2rkuLQdOc4uLlbYhXcKKd6WsZsnLt0,8953
158
- librelane/steps/odb.py,sha256=_WhAFEVbFioSGsVrGbXQVqcXYAnE22gLA4eF1v028EQ,38030
159
- librelane/steps/openroad.py,sha256=8YLMPq22ceG3TVRGghafuCD5a7DxwLtSSuPu4_7thPs,86513
158
+ librelane/steps/odb.py,sha256=WaYquQfdp0j5XSbDa7a_un0HF4bOprpuC6fFtKpHHI8,37956
159
+ librelane/steps/openroad.py,sha256=0nea0wtu197Ijqm6X8mGEbY_26xHVPNXcvJlDwOOWRc,86457
160
160
  librelane/steps/openroad_alerts.py,sha256=IJyB4piBDCKXhkJswHGMYCRDwbdQsR0GZlrGGDhmW6Q,3364
161
161
  librelane/steps/pyosys.py,sha256=mnbPR267XzJvDhtXW4cdTAB3IqvksUZt4ch5xQHgdY0,22705
162
- librelane/steps/step.py,sha256=OkFNyW86ZY9VlNQHUeJIOLHtOuO1bioXsnwky7_l1pc,55154
162
+ librelane/steps/step.py,sha256=M0Jqvr8sQaYmvXApZ-tfa4SFVOb-l_2ZfR31QaoRUf0,55742
163
163
  librelane/steps/tclstep.py,sha256=0PMWJ6C3dKnlQf9mA9rZntgxUBCiByE9csHcEcM1iq0,10027
164
164
  librelane/steps/verilator.py,sha256=MWx2TpLqYyea9_jSeLG9c2S5ujvYERQZRFNaMhfHxZE,7916
165
165
  librelane/steps/yosys.py,sha256=GX6rTiQG-ZhDxfB9SxrPQ9Sab3WC84p0OUtqiL1Nubk,12533
166
- librelane-2.4.0.dev10.dist-info/METADATA,sha256=meNmOy0WIvFs052fTV8herlmjP2wwBlTa8txR4Lf9Hg,6561
167
- librelane-2.4.0.dev10.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
168
- librelane-2.4.0.dev10.dist-info/entry_points.txt,sha256=GTBvXykNMMFsNKiJFgtEw7P1wb_VZIqVM35EFSpyZQE,263
169
- librelane-2.4.0.dev10.dist-info/RECORD,,
166
+ librelane-2.4.0.dev12.dist-info/METADATA,sha256=uB2QcYmogJ-ChrdE7S5esrt_iHQIJce5tqVLOM5Rrno,6561
167
+ librelane-2.4.0.dev12.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
168
+ librelane-2.4.0.dev12.dist-info/entry_points.txt,sha256=GTBvXykNMMFsNKiJFgtEw7P1wb_VZIqVM35EFSpyZQE,263
169
+ librelane-2.4.0.dev12.dist-info/RECORD,,