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,246 @@
1
+ # Copyright 2020-2023 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
+ import io
15
+ import re
16
+ import json
17
+ import shlex
18
+ from enum import IntEnum
19
+ from decimal import Decimal, InvalidOperation
20
+ from dataclasses import dataclass, field, asdict
21
+ from typing import List, Optional, Tuple, Dict
22
+
23
+ BoundingBox = Tuple[Decimal, Decimal, Decimal, Decimal] # microns
24
+
25
+
26
+ @dataclass
27
+ class Violation:
28
+ rules: List[Tuple[str, str]] # (layer, rule)
29
+ description: str
30
+ bounding_boxes: List[BoundingBox] = field(default_factory=list)
31
+
32
+ @property
33
+ def layer(self) -> str:
34
+ return self.rules[0][0]
35
+
36
+ @property
37
+ def rule(self) -> str:
38
+ return self.rules[0][1]
39
+
40
+ @property
41
+ def category_name(self) -> str:
42
+ return f"{self.layer}.{self.rule}"
43
+
44
+
45
+ illegal_overlap_rx = re.compile(r"between (\w+) and (\w+)")
46
+
47
+
48
+ @dataclass
49
+ class DRC:
50
+ """
51
+ A primitive database representing DRC violations generated by a design.
52
+ """
53
+
54
+ module: str
55
+ violations: Dict[str, Violation]
56
+
57
+ @classmethod
58
+ def from_magic(
59
+ Self,
60
+ report: io.TextIOWrapper,
61
+ ) -> Tuple["DRC", int]:
62
+ """
63
+ Parses a report generated by Magic into a DRC object.
64
+
65
+ :param report: A **text** input stream containing the report in question.
66
+ You can pass the result of ``open("drc.rpt")``, for example.
67
+ :returns: A tuple of the DRC object and an int representing the number
68
+ of DRC violations.
69
+ """
70
+
71
+ class State(IntEnum):
72
+ drc = 0
73
+ data = 1
74
+ header = 10
75
+
76
+ MAGIC_SPLIT_LINE = "-" * 40
77
+ MAGIC_RULE_LINE_PARSER = re.compile(r"^(.+?)(?:\s*\((.+)\))?$")
78
+ MAGIC_RULE_RX = re.compile(r"([\w\-]+)\.([\w\-]+)")
79
+
80
+ violations: Dict[str, Violation] = {}
81
+
82
+ violation: Optional[Violation] = None
83
+ state = State.header
84
+ module = "UNKNOWN"
85
+ counter = 0
86
+ bbox_count = 0
87
+ for i, line in enumerate(report):
88
+ line = line.strip()
89
+ if ("[INFO]" in line) or (line == ""):
90
+ continue
91
+
92
+ if MAGIC_SPLIT_LINE in line:
93
+ if state.value > 0:
94
+ violation = None
95
+ state = State.drc
96
+ else:
97
+ state = State.data
98
+ elif state == State.header:
99
+ module = line
100
+ elif state == State.drc:
101
+ match = MAGIC_RULE_LINE_PARSER.match(line)
102
+ assert match is not None, "universal regex did not match string"
103
+ description = match[0]
104
+ rules = []
105
+ if rules_raw := match[2]:
106
+ for match in MAGIC_RULE_RX.finditer(rules_raw):
107
+ layer = match[1]
108
+ rule = match[2]
109
+ rules.append((layer, rule))
110
+ if len(rules) == 0:
111
+ rules = [("UNKNOWN", f"UNKNOWN{counter}")]
112
+ violation = Violation(rules, description)
113
+ counter += 1
114
+ elif state == State.data:
115
+ assert violation is not None, "Parser reached an inconsistent state"
116
+ try:
117
+ coord_list = [Decimal(coord[:-2]) for coord in line.split()]
118
+ except InvalidOperation:
119
+ raise ValueError(
120
+ f"invalid bounding box at line {i}: number is invalid"
121
+ )
122
+
123
+ if len(coord_list) != 4:
124
+ raise ValueError(
125
+ f"invalid bounding box at line {i}: bounding box has {len(coord_list)}/4 elements"
126
+ )
127
+
128
+ bounding_box: BoundingBox = (
129
+ coord_list[0],
130
+ coord_list[1],
131
+ coord_list[2],
132
+ coord_list[3],
133
+ )
134
+
135
+ violation.bounding_boxes.append(bounding_box)
136
+ violations[violation.category_name] = violation
137
+ bbox_count += 1
138
+
139
+ return (Self(module, violations), bbox_count)
140
+
141
+ @classmethod
142
+ def from_magic_feedback(
143
+ Self, feedback: io.TextIOWrapper, cif_scale: Decimal, module: str
144
+ ) -> Tuple["DRC", int]:
145
+ bbox_count = 0
146
+ violations: Dict[str, Violation] = {}
147
+ last_bounding_box: Optional[BoundingBox] = None
148
+ lex = shlex.shlex(feedback.read(), posix=True)
149
+ components = list(lex)
150
+ while len(components):
151
+ instruction = components.pop(0)
152
+ if instruction == "box":
153
+ if len(components) < 4:
154
+ raise ValueError(
155
+ "Invalid syntax: 'box' command has less than 4 arguments"
156
+ )
157
+ lx, ly, ux, uy = components[0:4]
158
+ last_bounding_box = (
159
+ Decimal(lx) * cif_scale,
160
+ Decimal(ly) * cif_scale,
161
+ Decimal(ux) * cif_scale,
162
+ Decimal(uy) * cif_scale,
163
+ )
164
+ bbox_count += 1
165
+ components = components[4:]
166
+ elif instruction == "feedback":
167
+ try:
168
+ subcmd = components.pop(0)
169
+ except IndexError:
170
+ raise ValueError("feedback not given subcommand")
171
+ if subcmd != "add":
172
+ raise ValueError(f"Unsuppoorted feedback subcommand {subcmd}")
173
+
174
+ try:
175
+ rule = components.pop(0)
176
+ _ = components.pop(0)
177
+ except IndexError:
178
+ raise ValueError(
179
+ "Invalid syntax: 'feedback add' command has less than 2 arguments"
180
+ )
181
+ vio_layer = "UNKNOWN"
182
+ vio_rulenum = f"UNKNOWN{len(violations)}"
183
+ if "Illegal overlap" in rule:
184
+ vio_rulenum = "ILLEGAL_OVERLAP"
185
+ if match := illegal_overlap_rx.search(rule):
186
+ vio_layer = "-".join((match[1], match[2]))
187
+ if rule not in violations:
188
+ violations[rule] = Violation([(vio_layer, vio_rulenum)], rule, [])
189
+ if last_bounding_box is None:
190
+ raise ValueError("Attempted to add feedback without a box selected")
191
+ violations[rule].bounding_boxes.append(last_bounding_box)
192
+ violations = {vio.category_name: vio for vio in violations.values()}
193
+ return (Self(module, violations), bbox_count)
194
+
195
+ def dumps(self):
196
+ """
197
+ :returns: The DRC object as a JSON string.
198
+ """
199
+ return json.dumps(asdict(self))
200
+
201
+ def to_klayout_xml(self, out: io.BufferedIOBase):
202
+ """
203
+ Converts the DRC object to a KLayout-compatible XML database.
204
+
205
+ :param out: A **binary** output stream to the target XML file.
206
+ You can pass the result of ``open("drc.xml", "wb")``, for example.
207
+ """
208
+ from lxml import etree as ET
209
+
210
+ with ET.xmlfile(out, encoding="utf8", buffered=False) as xf:
211
+ xf.write_declaration()
212
+ with xf.element("report-database"):
213
+ # 1. Cells
214
+ with xf.element("cells"):
215
+ with xf.element("cell"):
216
+ name = ET.Element("name")
217
+ name.text = self.module
218
+ xf.write(name)
219
+ # 2. Categories
220
+ with xf.element("categories"):
221
+ for _, violation in self.violations.items():
222
+ with xf.element("category"):
223
+ name = ET.Element("name")
224
+ name.text = violation.category_name
225
+ description = ET.Element("description")
226
+ description.text = violation.description
227
+ xf.write(name, description)
228
+ # 3. Items
229
+ with xf.element("items"):
230
+ for _, violation in self.violations.items():
231
+ for bounding_box in violation.bounding_boxes:
232
+ with xf.element("item"):
233
+ cell = ET.Element("cell")
234
+ cell.text = self.module
235
+ category = ET.Element("category")
236
+ category.text = f"'{violation.category_name}'"
237
+ visited = ET.Element("visited")
238
+ visited.text = "false"
239
+ multiplicity = ET.Element("multiplicity")
240
+ multiplicity.text = str(len(violation.bounding_boxes))
241
+ xf.write(cell, category, visited, multiplicity)
242
+ with xf.element("values"):
243
+ llx, lly, urx, ury = bounding_box
244
+ value = ET.Element("value")
245
+ value.text = f"polygon: ({llx},{lly};{urx},{lly};{urx},{ury};{llx},{ury})"
246
+ xf.write(value)
@@ -0,0 +1,319 @@
1
+ # Copyright 2023 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
+ import os
15
+ import json
16
+ import dataclasses
17
+ from enum import Enum
18
+ from decimal import Decimal
19
+ from collections import UserString
20
+ from typing import (
21
+ Any,
22
+ Callable,
23
+ Dict,
24
+ Hashable,
25
+ ItemsView,
26
+ Iterator,
27
+ Mapping,
28
+ Sequence,
29
+ Type,
30
+ TypeVar,
31
+ Tuple,
32
+ Optional,
33
+ )
34
+
35
+ from .misc import idem
36
+ from .types import is_string
37
+
38
+
39
+ class GenericDictEncoder(json.JSONEncoder):
40
+ """
41
+ A JSON encoder for :class:`GenericDict` objects. Also handles some types not
42
+ necessarily handled by the default JSON encoder, i.e., UserString, os.PathLike,
43
+ Decimals, etcetera.
44
+
45
+ It is recommended to use :meth:`GenericDict.to_json` unless you know what
46
+ you're doing.
47
+ """
48
+
49
+ def default(self, o):
50
+ if isinstance(o, GenericDict):
51
+ return o.to_raw_dict()
52
+ elif isinstance(o, os.PathLike) or isinstance(o, UserString):
53
+ return str(o)
54
+ elif not isinstance(o, type) and dataclasses.is_dataclass(o):
55
+ return dataclasses.asdict(o)
56
+ elif isinstance(o, Enum):
57
+ return o.name
58
+ elif isinstance(o, Decimal):
59
+ if o.is_infinite() or o.as_integer_ratio()[1] != 1:
60
+ return float(o)
61
+ else:
62
+ return int(o)
63
+ return o
64
+
65
+
66
+ KT = TypeVar("KT", bound=Hashable)
67
+ VT = TypeVar("VT")
68
+
69
+
70
+ class GenericDict(Mapping[KT, VT]):
71
+ """
72
+ A dictionary with generic keys and values that is compatible with Python 3.8.1.
73
+
74
+ :param copying: A base Mapping object to copy values from.
75
+ :param overrides: Another mapping object to override the value from `copying`
76
+ with.
77
+ """
78
+
79
+ _data: Dict[KT, VT]
80
+
81
+ def __init__(
82
+ self,
83
+ copying: Optional[Mapping[KT, VT]] = None,
84
+ /,
85
+ overrides: Optional[Mapping[KT, VT]] = None,
86
+ ) -> None:
87
+ super().__init__()
88
+ self.__data = {}
89
+ copying = copying or {}
90
+ overrides = overrides or {}
91
+
92
+ for key, value in copying.items():
93
+ self.__data[key] = value
94
+ for key, value in overrides.items():
95
+ self.__data[key] = value
96
+
97
+ def __getitem__(self, key: KT) -> VT:
98
+ return self.__data[key]
99
+
100
+ def __setitem__(self, key: KT, item: VT):
101
+ self.__data[key] = item
102
+
103
+ def __delitem__(self, key: KT):
104
+ del self.__data[key]
105
+
106
+ def __len__(self) -> int:
107
+ return len(self.__data)
108
+
109
+ def __repr__(self) -> str:
110
+ return self.to_raw_dict().__repr__()
111
+
112
+ def __iter__(self) -> Iterator[KT]:
113
+ return iter(self.__data)
114
+
115
+ def __eq__(self, __o: object) -> bool:
116
+ if not (isinstance(__o, GenericDict) or isinstance(__o, dict)):
117
+ raise NotImplementedError()
118
+
119
+ rhs = __o
120
+ if isinstance(__o, GenericDict):
121
+ rhs = __o.to_raw_dict()
122
+
123
+ lhs = self.to_raw_dict()
124
+
125
+ return rhs == lhs
126
+
127
+ # ---
128
+ for key in set(self.keys()).union(__o.keys()):
129
+ if key not in self or key not in __o:
130
+ return False
131
+ if self[key] != __o[key]:
132
+ return False
133
+ return True
134
+
135
+ def pop(self, key: KT, /) -> VT:
136
+ """
137
+ :param key: The key to pop the value for.
138
+ :returns: The value for key. Raises ``IndexError`` if the key does not
139
+ exist. The key/value pair is then deleted from the dictionary.
140
+ """
141
+ value = self[key]
142
+ del self[key]
143
+ return value
144
+
145
+ T = TypeVar("T", bound="GenericDict")
146
+
147
+ def copy(self: T) -> T:
148
+ """
149
+ Convenience replacement for `object.__class__(object)`, which would
150
+ create a copy of the ``GenericDict`` object.
151
+
152
+ :returns: The copy
153
+ """
154
+ return self.__class__(self)
155
+
156
+ def to_raw_dict(self) -> dict:
157
+ """
158
+ :returns: A copy of the underlying Python built-in ``dict`` for this class.
159
+ """
160
+ return self.__data.copy()
161
+
162
+ def get_encoder(self) -> Type[GenericDictEncoder]:
163
+ """
164
+ :returns: A JSON encoder handling GenericDict objects.
165
+ """
166
+ return GenericDictEncoder
167
+
168
+ def keys(self):
169
+ """
170
+ :returns: A set-like object providing a view of the keys of the GenericDict object.
171
+ """
172
+ return self.__data.keys()
173
+
174
+ def values(self):
175
+ """
176
+ :returns: A set-like object providing a view of the values of the GenericDict object.
177
+ """
178
+ return self.__data.values()
179
+
180
+ def items(self) -> ItemsView[KT, VT]:
181
+ """
182
+ :returns: A set-like object providing a view of the GenericDict object as (key, value) tuples.
183
+ """
184
+ return self.__data.items()
185
+
186
+ def dumps(self, **kwargs) -> str:
187
+ """
188
+ :param kwargs: Passed to ``json.dumps``.
189
+ :returns: A JSON string representing the the GenericDict object.
190
+ """
191
+ if "indent" not in kwargs:
192
+ kwargs["indent"] = 4
193
+ return json.dumps(self.to_raw_dict(), cls=self.get_encoder(), **kwargs)
194
+
195
+ def check(self, key: KT, /) -> Tuple[Optional[KT], Optional[VT]]:
196
+ """
197
+ Checks if a key exists and returns a tuple in the form ``(key, value)``.
198
+
199
+ :param key: The key in question
200
+ :returns: If the key does not exist, the value of ``key`` will be ``None`` and so will
201
+ ``value``. If the key exists, ``key`` will be the key being checked for
202
+ existence and ``value`` will be the value assigned to said key in the
203
+ GenericDict object.
204
+
205
+ Do note ``None`` is a valid value for some keys, so simply
206
+ checking if the second element ``is not None`` is insufficient to check
207
+ whether a key exists.
208
+ """
209
+ return (key if key in self.__data else None, self.get(key))
210
+
211
+ def update(self, incoming: "Mapping[KT, VT]"):
212
+ """
213
+ A convenience function to update multiple values in the GenericDict object
214
+ at the same time.
215
+ :param incoming: The values to update
216
+ """
217
+ for key, value in incoming.items():
218
+ self[key] = value
219
+
220
+ def update_reorder(self, incoming: "Mapping[KT, VT]"):
221
+ """
222
+ A convenience function to update multiple values in the GenericDict object
223
+ at the same time. Pre-existing keys are deleted first so the values in
224
+ incoming are emplaced at the end of the dictionary.
225
+
226
+ :param incoming: The values to update
227
+ """
228
+ for key, value in incoming.items():
229
+ if key in self:
230
+ del self[key]
231
+ self[key] = value
232
+
233
+
234
+ class GenericImmutableDict(GenericDict[KT, VT]):
235
+ __lock: bool
236
+
237
+ def __init__(
238
+ self,
239
+ copying: Optional[Mapping[KT, VT]] = None,
240
+ /,
241
+ *args,
242
+ **kwargs,
243
+ ) -> None:
244
+ super().__init__(copying, *args, **kwargs)
245
+ self.__lock = True
246
+
247
+ def __setitem__(self, key: KT, item: VT):
248
+ if self.__lock:
249
+ raise TypeError(f"{self.__class__.__name__} is immutable")
250
+ return super().__setitem__(key, item)
251
+
252
+ def __delitem__(self, key: KT):
253
+ if self.__lock:
254
+ raise TypeError(f"{self.__class__.__name__} is immutable")
255
+ return super().__delitem__(key)
256
+
257
+ def __delattr__(self, attr: str):
258
+ if self.__lock:
259
+ raise TypeError(f"{self.__class__.__name__} is immutable")
260
+ return super().__delattr__(attr)
261
+
262
+ def __setattr__(self, attr: str, value: Any):
263
+ try:
264
+ if self.__lock:
265
+ raise TypeError(f"{self.__class__.__name__} is immutable")
266
+ except AttributeError:
267
+ pass
268
+ return super().__setattr__(attr, value)
269
+
270
+ def copy_mut(self) -> GenericDict[KT, VT]:
271
+ return GenericDict(self)
272
+
273
+
274
+ # Screw this, if you can figure out how to type hint mapping in dictionary out
275
+ # and non-mapping in sequence out in Python, be my guest
276
+ def copy_recursive(input, translator: Callable = idem):
277
+ """
278
+ Copies any arbitrarily-deep nested structure of Mappings and/or Sequences.
279
+
280
+ :param input: The input nested structure
281
+ :param translator: Before an object is appended, this function will be
282
+ called to process the value.
283
+
284
+ By default, :func:`idem` is called.
285
+ :returns: The copy.
286
+
287
+ All sequences will become built-in ``list``\\(s) and all mappings will
288
+ become built-in ``dict``\\(s).
289
+ """
290
+
291
+ def recursive(input, visit_stack: list, *, sequence_cls=list, mapping_cls=dict):
292
+ if id(input) in visit_stack:
293
+ raise ValueError("Circular reference found in target object")
294
+
295
+ visit_stack.append(id(input))
296
+
297
+ result: Any = input
298
+
299
+ if isinstance(input, Mapping): # Mappings are Sequences, but not vice versa
300
+ result = mapping_cls()
301
+ for key, value in input.items():
302
+ result[key] = recursive(value, visit_stack)
303
+ elif dataclasses.is_dataclass(input) and not isinstance(input, type):
304
+ replace = {}
305
+ as_dict = dataclasses.asdict(input)
306
+ for key, value in as_dict.items():
307
+ replace[key] = recursive(value, visit_stack)
308
+ result = dataclasses.replace(input, **replace)
309
+ elif not is_string(input) and isinstance(input, Sequence):
310
+ result = sequence_cls()
311
+ for value in input:
312
+ result.append(recursive(value, visit_stack))
313
+
314
+ visit_stack.pop()
315
+
316
+ result = translator(result)
317
+ return result
318
+
319
+ return recursive(input, [])
@@ -0,0 +1,35 @@
1
+ # Copyright 2023 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
+ # Copyright 2023 Efabless Corporation
15
+ #
16
+ # Licensed under the Apache License, Version 2.0 (the "License");
17
+ # you may not use this file except in compliance with the License.
18
+ # You may obtain a copy of the License at
19
+ #
20
+ # http://www.apache.org/licenses/LICENSE-2.0
21
+ #
22
+ # Unless required by applicable law or agreed to in writing, software
23
+ # distributed under the License is distributed on an "AS IS" BASIS,
24
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
25
+ # See the License for the specific language governing permissions and
26
+ # limitations under the License.
27
+ """
28
+ Metrics Module
29
+ -----------------------
30
+
31
+ Classes and functions for dealing with Metrics based on the `METRICS2.1 <https://github.com/ieee-ceda-datc/datc-rdf-Metrics4ML>`_ standard.
32
+ """
33
+ from . import library
34
+ from .metric import MetricAggregator, MetricComparisonResult, Metric
35
+ from .util import parse_metric_modifiers, aggregate_metrics, MetricDiff