diff --git a/edg/BoardCompiler.py b/edg/BoardCompiler.py index b056369f4..1f7ef6eaa 100644 --- a/edg/BoardCompiler.py +++ b/edg/BoardCompiler.py @@ -3,6 +3,7 @@ from contextlib import suppress from typing import Type, Optional, Tuple +from . import edgir from .core import Block, ScalaCompiler, CompiledDesign, CompiledDesignExportTransform from .electronics_model.NetlistBackend import NetlistBackend # imported separately b/c mypy confuses with the modules from .electronics_model.SvgPcbBackend import SvgPcbBackend @@ -18,15 +19,16 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str] assert os.path.isdir(target_dir), f"target_dir {target_dir} to compile_board must be directory" design_filename = os.path.join(target_dir, f"{target_name}.edg") - netlist_filename = os.path.join(target_dir, f"{target_name}.net") + netlist_filename_prefix = os.path.join(target_dir, f"{target_name}") bom_filename = os.path.join(target_dir, f"{target_name}.csv") svgpcb_filename = os.path.join(target_dir, f"{target_name}.svgpcb.js") compiled_json_filename = os.path.join(target_dir, f"{target_name}.compiled.json") with suppress(FileNotFoundError): os.remove(design_filename) - with suppress(FileNotFoundError): - os.remove(netlist_filename) + for filename in os.listdir(target_dir): + if filename.startswith(target_name) and filename.endswith(".net"): + os.remove(os.path.join(target_dir, filename)) with suppress(FileNotFoundError): os.remove(bom_filename) with suppress(FileNotFoundError): @@ -48,13 +50,21 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str] netlist_all = NetlistBackend().run(compiled) bom_all = GenerateBom().run(compiled) + assert len(bom_all) == 1 svgpcb_all = SvgPcbBackend().run(compiled) + assert len(svgpcb_all) == 1 compiled_json = CompiledDesignExportTransform(compiled).transform() - assert len(netlist_all) == 1 if target_dir_name is not None: - with open(netlist_filename, "w", encoding="utf-8") as net_file: - net_file.write(netlist_all[0][1]) + for path, netlist in netlist_all: + path_str = edgir.local_path_to_str_list(path) + if not path_str: + net_filename = netlist_filename_prefix + ".net" + else: + net_filename = netlist_filename_prefix + "_" + "_".join(path_str) + ".net" + + with open(net_filename, "w", encoding="utf-8") as net_file: + net_file.write(netlist) with open(bom_filename, "w", encoding="utf-8") as bom_file: bom_file.write(bom_all[0][1]) diff --git a/edg/abstract_parts/test_kicad_import_netlist.py b/edg/abstract_parts/test_kicad_import_netlist.py index b88bd11ed..c26ea75af 100644 --- a/edg/abstract_parts/test_kicad_import_netlist.py +++ b/edg/abstract_parts/test_kicad_import_netlist.py @@ -39,7 +39,7 @@ def __init__(self) -> None: class KiCadImportBlackboxTestCase(unittest.TestCase): def test_netlist(self) -> None: - net = NetlistTestCase.generate_net( + net = NetlistTestCase.generate_single_net( KiCadBlackboxTop, refinements=Refinements( class_refinements=[ diff --git a/edg/electronics_model/BoardScopedTransform.py b/edg/electronics_model/BoardScopedTransform.py index c250ae0e5..b82ee69f2 100644 --- a/edg/electronics_model/BoardScopedTransform.py +++ b/edg/electronics_model/BoardScopedTransform.py @@ -15,9 +15,10 @@ class BoardScopedTransform(TransformUtil.Transform): def __init__(self, design: CompiledDesign) -> None: super().__init__() self._design = design - self._board_scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = { + self._board_parent_scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = { TransformUtil.Path.empty(): TransformUtil.Path.empty() } # always initialized in parent + self._board_scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = {} def visit_block_scoped( self, context: TransformUtil.TransformContext, scope: Optional[TransformUtil.Path], block: edgir.BlockTypes @@ -36,7 +37,7 @@ def visit_linkarray_scoped( @override def visit_block(self, context: TransformContext, block: edgir.HierarchyBlock) -> None: - scope = self._board_scopes[context.path] + parent_scope = self._board_parent_scopes[context.path] if "fp_subboard" in block.meta.members.node: fp_external_blocks = self._design.get_value(context.path.to_tuple() + ("fp_external_blocks",)) @@ -48,15 +49,17 @@ def visit_block(self, context: TransformContext, block: edgir.HierarchyBlock) -> internal_scope = context.path else: external_blocks = None - internal_scope = scope + internal_scope = parent_scope + + self._board_scopes[context.path] = internal_scope for block_pair in block.blocks: if external_blocks is not None and block_pair.name not in external_blocks: - self._board_scopes[context.path.append_block(block_pair.name)] = internal_scope + self._board_parent_scopes[context.path.append_block(block_pair.name)] = internal_scope else: - self._board_scopes[context.path.append_block(block_pair.name)] = scope + self._board_parent_scopes[context.path.append_block(block_pair.name)] = parent_scope - self.visit_block_scoped(context, scope, block) + self.visit_block_scoped(context, internal_scope, block) @override def visit_link(self, context: TransformContext, link: edgir.Link) -> None: diff --git a/edg/electronics_model/NetlistBackend.py b/edg/electronics_model/NetlistBackend.py index dc64f564c..c98a2884c 100644 --- a/edg/electronics_model/NetlistBackend.py +++ b/edg/electronics_model/NetlistBackend.py @@ -21,7 +21,8 @@ def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[e else: raise ValueError(f"Invalid RefdesMode value {refdes_mode_arg}") - netlist = NetlistTransform(design).run() - netlist_string = kicad.generate_netlist(netlist, refdes_mode) - - return [(edgir.LocalPath(), netlist_string)] + board_netlists = NetlistTransform(design).run() + return [ + (netlist_path.to_local_path(), kicad.generate_netlist(netlist, refdes_mode)) + for (netlist_path, netlist) in board_netlists.items() + ] diff --git a/edg/electronics_model/NetlistGenerator.py b/edg/electronics_model/NetlistGenerator.py index d41ab023d..9856358fd 100644 --- a/edg/electronics_model/NetlistGenerator.py +++ b/edg/electronics_model/NetlistGenerator.py @@ -58,7 +58,6 @@ def empty(cls, path: TransformUtil.Path) -> "BoardScope": # returns a fresh, em return BoardScope(path, {}, {}, {}, []) -Scopes = Dict[TransformUtil.Path, Optional[BoardScope]] # Block -> board scope (reference, aliased across entries) ClassPaths = Dict[ TransformUtil.Path, List[edgir.LibraryPath] ] # Path -> class names corresponding to shortened path name @@ -82,8 +81,9 @@ def flatten_port(path: TransformUtil.Path, port: edgir.PortLike) -> Iterable[Tra def __init__(self, design: CompiledDesign): super().__init__(design) - self.all_scopes = [BoardScope.empty(TransformUtil.Path.empty())] # list of unique scopes - self.scopes: Scopes = {TransformUtil.Path.empty(): self.all_scopes[0]} + self.scopes: Dict[TransformUtil.Path, Optional[BoardScope]] = { + TransformUtil.Path.empty(): BoardScope.empty(TransformUtil.Path.empty()) + } # board scope path to scope object self.class_paths: ClassPaths = {TransformUtil.Path.empty(): []} # seed root self.path_traverse_order: List[TransformUtil.Path] = [] @@ -98,6 +98,16 @@ def process_blocklike( else: scope_obj = None + if isinstance(block, edgir.HierarchyBlock) and "fp_subboard" in block.meta.members.node: + # only valid for sub-board blocks, where some things happen in the parent scope + parent_scope = self._board_parent_scopes[path] + if parent_scope is not None: + parent_scope_obj: Optional[BoardScope] = self.scopes[parent_scope] + else: + parent_scope_obj = None + else: + parent_scope_obj = None + if isinstance(block, edgir.HierarchyBlock): class_path = self.class_paths[path] for block_pair in block.blocks: @@ -171,66 +181,85 @@ def process_blocklike( scope_obj.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named scope_obj.pins.setdefault(src_path, []).append(NetPin(path, pin_name)) - for constraint_pair in block.constraints: - if scope_obj is not None: + # for blocks with mixed scope, connections happen in both scopes, since blocks may be in either + # this may cause inner connections to leach out into the parent scope, or + # result in extraneous connections in either scope, + # but is much simpler implementation-wise + all_scopes = [] + if scope_obj is not None: + all_scopes.append(scope_obj) + if parent_scope_obj is not None and parent_scope_obj is not scope_obj: + all_scopes.append(parent_scope_obj) + + if all_scopes: + for constraint_pair in block.constraints: if constraint_pair.value.HasField("connected"): - self.process_connected(path, block, scope_obj, constraint_pair.value.connected) + self.process_connected(path, block, all_scopes, constraint_pair.value.connected) elif constraint_pair.value.HasField("connectedArray"): for expanded_connect in constraint_pair.value.connectedArray.expanded: - self.process_connected(path, block, scope_obj, expanded_connect) + self.process_connected(path, block, all_scopes, expanded_connect) elif constraint_pair.value.HasField("exported"): - self.process_exported(path, block, scope_obj, constraint_pair.value.exported) + self.process_exported(path, block, all_scopes, constraint_pair.value.exported) elif constraint_pair.value.HasField("exportedArray"): for expanded_export in constraint_pair.value.exportedArray.expanded: - self.process_exported(path, block, scope_obj, expanded_export) + self.process_exported(path, block, all_scopes, expanded_export) elif constraint_pair.value.HasField("exportedTunnel"): - self.process_exported(path, block, scope_obj, constraint_pair.value.exportedTunnel) + self.process_exported(path, block, all_scopes, constraint_pair.value.exportedTunnel) def process_connected( - self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope, constraint: edgir.ConnectedExpr + self, + path: TransformUtil.Path, + current: edgir.EltTypes, + scopes: List[BoardScope], + constraint: edgir.ConnectedExpr, ) -> None: if constraint.expanded: assert len(constraint.expanded) == 1 - self.process_connected(path, current, scope, constraint.expanded[0]) + self.process_connected(path, current, scopes, constraint.expanded[0]) return assert constraint.block_port.HasField("ref") assert constraint.link_port.HasField("ref") self.connect_ports( - scope, path.follow(constraint.block_port.ref, current), path.follow(constraint.link_port.ref, current) + scopes, path.follow(constraint.block_port.ref, current), path.follow(constraint.link_port.ref, current) ) def process_exported( - self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope, constraint: edgir.ExportedExpr + self, + path: TransformUtil.Path, + current: edgir.EltTypes, + scopes: List[BoardScope], + constraint: edgir.ExportedExpr, ) -> None: if constraint.expanded: assert len(constraint.expanded) == 1 - self.process_exported(path, current, scope, constraint.expanded[0]) + self.process_exported(path, current, scopes, constraint.expanded[0]) return assert constraint.internal_block_port.HasField("ref") assert constraint.exterior_port.HasField("ref") self.connect_ports( - scope, + scopes, path.follow(constraint.internal_block_port.ref, current), path.follow(constraint.exterior_port.ref, current), ) def connect_ports( self, - scope: BoardScope, + scopes: List[BoardScope], elt1: Tuple[TransformUtil.Path, edgir.EltTypes], elt2: Tuple[TransformUtil.Path, edgir.EltTypes], ) -> None: """Recursively connect ports, including containers and leaf ports. Net-ness is ignored here.""" if isinstance(elt1[1], edgir.Port) and isinstance(elt2[1], edgir.Port): - scope.edges.setdefault(elt1[0], []).append(elt2[0]) - scope.edges.setdefault(elt2[0], []).append(elt1[0]) + for scope in scopes: + scope.edges.setdefault(elt1[0], []).append(elt2[0]) + scope.edges.setdefault(elt2[0], []).append(elt1[0]) elt1_names = list(map(lambda pair: pair.name, elt1[1].ports)) elt2_names = list(map(lambda pair: pair.name, elt2[1].ports)) assert elt1_names == elt2_names, f"mismatched port sub-ports in types {elt1}, {elt2}" for key in elt2_names: self.connect_ports( - scope, + scopes, (elt1[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt1[1].ports, key))), (elt2[0].append_port(key), edgir.resolve_portlike(edgir.pair_get(elt2[1].ports, key))), ) @@ -371,11 +400,10 @@ def port_ignored_paths(path: TransformUtil.Path) -> bool: # ignore link ports f return Netlist(netlist_footprints, netlist_nets) - def run(self) -> Netlist: + def run(self) -> Dict[TransformUtil.Path, Netlist]: self.transform_design(self._design.design) - assert len(self.all_scopes) == 1, "TODO: support multiple boards" - return self.scope_to_netlist(self.all_scopes[0]) + return {path: self.scope_to_netlist(scope) for path, scope in self.scopes.items() if scope is not None} class PathShortener: diff --git a/edg/electronics_model/RefdesRefinementPass.py b/edg/electronics_model/RefdesRefinementPass.py index 851601856..5f0770078 100644 --- a/edg/electronics_model/RefdesRefinementPass.py +++ b/edg/electronics_model/RefdesRefinementPass.py @@ -30,7 +30,7 @@ def __init__(self, design: CompiledDesign): self.board_refdes_prefix = board_refdes_prefix self.block_refdes_list: List[Tuple[TransformUtil.Path, str]] = [] # populated in traversal order - self.refdes_last: Dict[Tuple[TransformUtil.Path, str], int] = {} # (scope, prefix) -> num + self.refdes_last: Dict[str, int] = {} # prefix -> num @override def visit_block_scoped( @@ -40,8 +40,8 @@ def visit_block_scoped( refdes_prefix = self._design.get_value(context.path.to_tuple() + ("fp_refdes_prefix",)) assert isinstance(refdes_prefix, str) - refdes_id = self.refdes_last.get((scope, refdes_prefix), 0) + 1 - self.refdes_last[(scope, refdes_prefix)] = refdes_id + refdes_id = self.refdes_last.get(refdes_prefix, 0) + 1 + self.refdes_last[refdes_prefix] = refdes_id self.block_refdes_list.append((context.path, self.board_refdes_prefix + refdes_prefix + str(refdes_id))) def run(self) -> List[Tuple[TransformUtil.Path, str]]: diff --git a/edg/electronics_model/SvgPcbBackend.py b/edg/electronics_model/SvgPcbBackend.py index 8f64c8969..5e95ed12f 100644 --- a/edg/electronics_model/SvgPcbBackend.py +++ b/edg/electronics_model/SvgPcbBackend.py @@ -190,7 +190,7 @@ def run(self) -> List[SvgPcbGeneratedBlock]: class SvgPcbBackend(BaseBackend): @override def run(self, design: CompiledDesign, args: Dict[str, str] = {}) -> List[Tuple[edgir.LocalPath, str]]: - netlist = NetlistTransform(design).run() + netlist = NetlistTransform(design).run()[TransformUtil.Path.empty()] # only generate for top-level board result = self._generate(design, netlist) return [(edgir.LocalPath(), result)] @@ -213,7 +213,7 @@ def filter_blocks_by_pathname( svgpcb_block_bboxes = [BlackBoxBlock(block.path, block.bbox) for block in svgpcb_blocks] # handle footprints - netlist = NetlistTransform(design).run() + netlist = NetlistTransform(design).run()[TransformUtil.Path.empty()] # only generate for top-level board svgpcb_block_prefixes = [block.path.to_tuple() for block in svgpcb_blocks] other_blocks = filter_blocks_by_pathname(netlist.blocks, svgpcb_block_prefixes) arranged_blocks = arrange_blocks(other_blocks, svgpcb_block_bboxes) diff --git a/edg/electronics_model/test_bundle_netlist.py b/edg/electronics_model/test_bundle_netlist.py index b93b25799..3c0a81a42 100644 --- a/edg/electronics_model/test_bundle_netlist.py +++ b/edg/electronics_model/test_bundle_netlist.py @@ -123,7 +123,7 @@ def contents(self) -> None: class BundleNetlistTestCase(unittest.TestCase): def test_spi_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestSpiCircuit) + net = NetlistTestCase.generate_single_net(TestSpiCircuit) self.assertIn( Net( @@ -237,7 +237,7 @@ def test_spi_netlist(self) -> None: ) def test_uart_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestUartCircuit) + net = NetlistTestCase.generate_single_net(TestUartCircuit) self.assertIn( Net( @@ -285,7 +285,7 @@ def test_uart_netlist(self) -> None: ) def test_can_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestCanCircuit) + net = NetlistTestCase.generate_single_net(TestCanCircuit) self.assertIn( Net( diff --git a/edg/electronics_model/test_multipack_netlist.py b/edg/electronics_model/test_multipack_netlist.py index 4e12788f3..5710f1aea 100644 --- a/edg/electronics_model/test_multipack_netlist.py +++ b/edg/electronics_model/test_multipack_netlist.py @@ -78,7 +78,7 @@ def multipack(self) -> None: class MultipackNetlistTestCase(unittest.TestCase): def test_packed_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestPackedDevices) + net = NetlistTestCase.generate_single_net(TestPackedDevices) self.assertIn( Net( @@ -152,4 +152,4 @@ def test_invalid_netlist(self) -> None: from .NetlistGenerator import InvalidPackingException with self.assertRaises(InvalidPackingException): - NetlistTestCase.generate_net(TestInvalidPackedDevices) + NetlistTestCase.generate_single_net(TestInvalidPackedDevices) diff --git a/edg/electronics_model/test_netlist.py b/edg/electronics_model/test_netlist.py index 6e0246184..a4f452a21 100644 --- a/edg/electronics_model/test_netlist.py +++ b/edg/electronics_model/test_netlist.py @@ -197,13 +197,15 @@ def contents(self) -> None: class NetlistTestCase(unittest.TestCase): @staticmethod - def generate_net(design: Type[Block], refinements: Refinements = Refinements()) -> Netlist: + def generate_single_net(design: Type[Block], refinements: Refinements = Refinements()) -> Netlist: compiled = ScalaCompiler.compile(design, refinements) compiled.append_values(RefdesRefinementPass().run(compiled)) - return NetlistTransform(compiled).run() + board_netlists = NetlistTransform(compiled).run() + assert len(board_netlists) == 1, "expected exactly one netlist from design" + return list(board_netlists.values())[0] def test_single_netlist(self) -> None: - net = self.generate_net(TestSinglePart) + net = self.generate_single_net(TestSinglePart) # check that the top-level path element is never pruned, even when the design is one element self.assertIn( @@ -219,7 +221,7 @@ def test_single_netlist(self) -> None: ) def test_basic_netlist(self) -> None: - net = self.generate_net(TestBasicCircuit) + net = self.generate_single_net(TestBasicCircuit) self.assertIn( Net( @@ -267,7 +269,7 @@ def test_basic_netlist(self) -> None: ) def test_multisink_netlist(self) -> None: - net = self.generate_net(TestMultisinkCircuit) + net = self.generate_single_net(TestMultisinkCircuit) self.assertIn( Net( @@ -328,7 +330,7 @@ def test_multisink_netlist(self) -> None: ) def test_multinet_netlist(self) -> None: - net = self.generate_net(TestMultinetCircuit) + net = self.generate_single_net(TestMultinetCircuit) self.assertIn( Net( @@ -399,7 +401,7 @@ def test_multinet_netlist(self) -> None: ) def test_hierarchy_netlist(self) -> None: - net = self.generate_net(TestHierarchyCircuit) + net = self.generate_single_net(TestHierarchyCircuit) self.assertIn( Net( @@ -452,7 +454,7 @@ def test_hierarchy_netlist(self) -> None: ) def test_dual_hierarchy_netlist(self) -> None: - net = self.generate_net(TestDualHierarchyCircuit) + net = self.generate_single_net(TestDualHierarchyCircuit) self.assertIn( Net( diff --git a/edg/electronics_model/test_netlist_subboard.py b/edg/electronics_model/test_netlist_subboard.py index 1bf8d16d7..d1aa9ebd6 100644 --- a/edg/electronics_model/test_netlist_subboard.py +++ b/edg/electronics_model/test_netlist_subboard.py @@ -2,13 +2,14 @@ from typing_extensions import override -from .. import FootprintBlock, DesignTop +from .NetlistGenerator import NetlistTransform +from .. import FootprintBlock, DesignTop, ScalaCompiler, RefdesRefinementPass from ..core import TransformUtil -from .test_netlist import NetlistTestCase, TestFakeSource, TestFakeSink, NetBlock, Net, NetPin -from . import SubboardBlock, WrapperSubboardBlock, VoltageSink, Passive +from .test_netlist import TestFakeSource, TestFakeSink, NetBlock, Net, NetPin +from . import SubboardBlock, VoltageSink -class SinkWrapperExterior(FootprintBlock): +class SinkSubboardExterior(FootprintBlock): def __init__(self) -> None: super().__init__() @@ -20,15 +21,15 @@ def contents(self) -> None: super().contents() self.footprint( # only this footprint shows up - "L", - "Inductor_SMD:L_0603_1608Metric", # distinct footprint and value from internal blocks + "R", + "Resistor_SMD:R_0603_1608Metric", {"1": self.pos, "2": self.neg}, - value="100", + value="200", ) -class SinkWrapperBlock(WrapperSubboardBlock): - """Wrapper block with a single footprint for two internal sinks whose footprints are ignored.""" +class SinkSubboardBlock(SubboardBlock): + """Subboard block with a single footprint for two internal sinks whose footprints are ignored.""" def __init__(self) -> None: super().__init__() @@ -40,104 +41,53 @@ def __init__(self) -> None: def contents(self) -> None: super().contents() - # these define the modeling and are internal - self.model1 = self.Block(TestFakeSink()) - self.model2 = self.Block(TestFakeSink()) - self.vpos = self.connect(self.pos, self.model1.pos, self.model2.pos) - self.gnd = self.connect(self.neg, self.model1.neg, self.model2.neg) + # these blocks are part of the sub-board + self.inner1 = self.Block(TestFakeSink()) + self.inner2 = self.Block(TestFakeSink()) + self.vpos = self.connect(self.pos, self.inner1.pos, self.inner2.pos) + self.gnd = self.connect(self.neg, self.inner1.neg, self.inner2.neg) # these define the external interface block - self.wrapper = self.Block(SinkWrapperExterior(), external=True) + self.wrapper = self.Block(SinkSubboardExterior(), external=True) self.export_tap(self.pos, self.wrapper.pos) self.export_tap(self.neg, self.wrapper.neg) -class TestWrapperCircuit(DesignTop): +class TestSubboardCircuit(DesignTop): @override def contents(self) -> None: super().contents() self.source = self.Block(TestFakeSource()) - self.sink = self.Block(SinkWrapperBlock()) + self.sink = self.Block(SinkSubboardBlock()) self.vpos = self.connect(self.source.pos, self.sink.pos) self.gnd = self.connect(self.source.neg, self.sink.neg) -class SinkWrapperPassiveExterior(FootprintBlock): - def __init__(self) -> None: - super().__init__() - - self.pos = self.Port(Passive.empty()) # must remain empty - self.neg = self.Port(Passive.empty()) - - @override - def contents(self) -> None: - super().contents() - - self.footprint( # only this footprint shows up - "L", - "Inductor_SMD:L_0603_1608Metric", # distinct footprint and value from internal blocks - {"1": self.pos, "2": self.neg}, - value="100", - ) - - -class SinkWrapperPassiveBlock(WrapperSubboardBlock): - """Wrapper block that taps the passive sub-port on its external port.""" - - def __init__(self) -> None: - super().__init__() - - self.pos = self.Port(VoltageSink.empty()) - self.neg = self.Port(VoltageSink.empty()) - - @override - def contents(self) -> None: - super().contents() - - # these define the modeling and are internal - self.model = self.Block(TestFakeSink()) - self.vpos = self.connect(self.pos, self.model.pos) - self.gnd = self.connect(self.neg, self.model.neg) - - # these define the external interface block - self.wrapper = self.Block(SinkWrapperPassiveExterior(), external=True) - self.export_tap(self.pos.net, self.wrapper.pos) - self.export_tap(self.neg.net, self.wrapper.neg) - - -class TestWrapperPassiveCircuit(DesignTop): - @override - def contents(self) -> None: - super().contents() - - self.source = self.Block(TestFakeSource()) - self.sink = self.Block(SinkWrapperPassiveBlock()) - - self.vpos = self.connect(self.source.pos, self.sink.pos) - self.gnd = self.connect(self.source.neg, self.sink.neg) +class NetlistSubboardTestCase(unittest.TestCase): + def test_subboard_netlist(self) -> None: + compiled = ScalaCompiler.compile(TestSubboardCircuit) + compiled.append_values(RefdesRefinementPass().run(compiled)) + board_netlists = NetlistTransform(compiled).run() - -class NetlistWrapperTestCase(unittest.TestCase): - def test_wrapper_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestWrapperCircuit) + top_net = board_netlists[TransformUtil.Path.empty()] self.assertIn( NetBlock( - "Inductor_SMD:L_0603_1608Metric", - "L1", + "Resistor_SMD:R_0603_1608Metric", + "R3", "", - "100", + "200", ["sink", "wrapper"], [ - "edg.electronics_model.test_netlist_subboard.SinkWrapperBlock", - "edg.electronics_model.test_netlist_subboard.SinkWrapperExterior", + "edg.electronics_model.test_netlist_subboard.SinkSubboardBlock", + "edg.electronics_model.test_netlist_subboard.SinkSubboardExterior", ], ), - net.blocks, + top_net.blocks, ) - self.assertEqual(len(net.blocks), 2) # should only generate top-level source and sink + self.assertEqual(len(top_net.blocks), 2) # should only generate top-level source and sink self.assertIn( Net( @@ -149,7 +99,7 @@ def test_wrapper_netlist(self) -> None: TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("pos", "net"), ], ), - net.nets, + top_net.nets, ) self.assertIn( Net( @@ -161,53 +111,65 @@ def test_wrapper_netlist(self) -> None: TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("neg", "net"), ], ), - net.nets, + top_net.nets, ) - self.assertEqual(len(net.nets), 2) # ensure empty nets pruned - - def test_wrapper_passive_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestWrapperPassiveCircuit) + self.assertEqual(len(top_net.nets), 2) # ensure empty nets pruned + inner_net = board_netlists[TransformUtil.Path.empty().append_block("sink")] self.assertIn( NetBlock( - "Inductor_SMD:L_0603_1608Metric", - "L1", + "Resistor_SMD:R_0603_1608Metric", + "R1", "", - "100", - ["sink", "wrapper"], + "1k", + ["sink", "inner1"], [ - "edg.electronics_model.test_netlist_subboard.SinkWrapperPassiveBlock", - "edg.electronics_model.test_netlist_subboard.SinkWrapperPassiveExterior", + "edg.electronics_model.test_netlist_subboard.SinkSubboardBlock", + "edg.electronics_model.test_netlist.TestFakeSink", ], ), - net.blocks, + inner_net.blocks, ) - self.assertEqual(len(net.blocks), 2) # should only generate top-level source and sink + self.assertIn( + NetBlock( + "Resistor_SMD:R_0603_1608Metric", + "R2", + "", + "1k", + ["sink", "inner2"], + [ + "edg.electronics_model.test_netlist_subboard.SinkSubboardBlock", + "edg.electronics_model.test_netlist.TestFakeSink", + ], + ), + inner_net.blocks, + ) + self.assertEqual(len(inner_net.blocks), 2) self.assertIn( Net( - "vpos", - [NetPin(["source"], "1"), NetPin(["sink", "wrapper"], "1")], # ensure extraneous nets not generated + "sink.vpos", + [NetPin(["sink", "inner1"], "1"), NetPin(["sink", "inner2"], "1")], [ - TransformUtil.Path.empty().append_block("source").append_port("pos", "net"), TransformUtil.Path.empty().append_block("sink").append_port("pos", "net"), - TransformUtil.Path.empty().append_block("sink", "model").append_port("pos", "net"), - TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("pos"), + TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink", "inner1").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink", "inner2").append_port("pos", "net"), ], ), - net.nets, + inner_net.nets, ) self.assertIn( Net( - "gnd", - [NetPin(["source"], "2"), NetPin(["sink", "wrapper"], "2")], + "sink.gnd", + [NetPin(["sink", "inner1"], "2"), NetPin(["sink", "inner2"], "2")], [ - TransformUtil.Path.empty().append_block("source").append_port("neg", "net"), TransformUtil.Path.empty().append_block("sink").append_port("neg", "net"), - TransformUtil.Path.empty().append_block("sink", "model").append_port("neg", "net"), - TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("neg"), + TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink", "inner1").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink", "inner2").append_port("neg", "net"), ], ), - net.nets, + inner_net.nets, ) - self.assertEqual(len(net.nets), 2) # ensure empty nets pruned + self.assertEqual(len(inner_net.nets), 2) # ensure empty nets pruned diff --git a/edg/electronics_model/test_netlist_subboard_array.py b/edg/electronics_model/test_netlist_subboard_array.py index 9e350dfe7..7b10f2f4d 100644 --- a/edg/electronics_model/test_netlist_subboard_array.py +++ b/edg/electronics_model/test_netlist_subboard_array.py @@ -78,7 +78,7 @@ def contents(self) -> None: class NetlistArrayWrapperTestCase(unittest.TestCase): def test_wrapper_netlist(self) -> None: - net = NetlistTestCase.generate_net(TestArrayWrapperCircuit) + net = NetlistTestCase.generate_single_net(TestArrayWrapperCircuit) self.assertIn( NetBlock( diff --git a/edg/electronics_model/test_netlist_subboard_wrapper.py b/edg/electronics_model/test_netlist_subboard_wrapper.py new file mode 100644 index 000000000..8ff9525a7 --- /dev/null +++ b/edg/electronics_model/test_netlist_subboard_wrapper.py @@ -0,0 +1,214 @@ +import unittest + +from typing_extensions import override + +from .. import FootprintBlock, DesignTop +from ..core import TransformUtil +from .test_netlist import NetlistTestCase, TestFakeSource, TestFakeSink, NetBlock, Net, NetPin +from . import WrapperSubboardBlock, VoltageSink, Passive + + +class SinkWrapperExterior(FootprintBlock): + def __init__(self) -> None: + super().__init__() + + self.pos = self.Port(VoltageSink.empty()) # must remain empty + self.neg = self.Port(VoltageSink.empty()) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( # only this footprint shows up + "L", + "Inductor_SMD:L_0603_1608Metric", # distinct footprint and value from internal blocks + {"1": self.pos, "2": self.neg}, + value="100", + ) + + +class SinkWrapperBlock(WrapperSubboardBlock): + """Wrapper block with a single footprint for two internal sinks whose footprints are ignored.""" + + def __init__(self) -> None: + super().__init__() + + self.pos = self.Port(VoltageSink.empty()) + self.neg = self.Port(VoltageSink.empty()) + + @override + def contents(self) -> None: + super().contents() + + # these define the modeling and are internal + self.model = self.Block(TestFakeSink()) + self.vpos = self.connect(self.pos, self.model.pos) + self.gnd = self.connect(self.neg, self.model.neg) + + # these define the external interface block + self.wrapper = self.Block(SinkWrapperExterior(), external=True) + self.export_tap(self.pos, self.wrapper.pos) + self.export_tap(self.neg, self.wrapper.neg) + + +class TestWrapperCircuit(DesignTop): + @override + def contents(self) -> None: + super().contents() + + self.source = self.Block(TestFakeSource()) + self.sink = self.Block(SinkWrapperBlock()) + + self.vpos = self.connect(self.source.pos, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.sink.neg) + + +class SinkWrapperPassiveExterior(FootprintBlock): + def __init__(self) -> None: + super().__init__() + + self.pos = self.Port(Passive.empty()) # must remain empty + self.neg = self.Port(Passive.empty()) + + @override + def contents(self) -> None: + super().contents() + + self.footprint( # only this footprint shows up + "L", + "Inductor_SMD:L_0603_1608Metric", # distinct footprint and value from internal blocks + {"1": self.pos, "2": self.neg}, + value="100", + ) + + +class SinkWrapperPassiveBlock(WrapperSubboardBlock): + """Wrapper block that taps the passive sub-port on its external port.""" + + def __init__(self) -> None: + super().__init__() + + self.pos = self.Port(VoltageSink.empty()) + self.neg = self.Port(VoltageSink.empty()) + + @override + def contents(self) -> None: + super().contents() + + # these define the modeling and are internal + self.model = self.Block(TestFakeSink()) + self.vpos = self.connect(self.pos, self.model.pos) + self.gnd = self.connect(self.neg, self.model.neg) + + # these define the external interface block + self.wrapper = self.Block(SinkWrapperPassiveExterior(), external=True) + self.export_tap(self.pos.net, self.wrapper.pos) + self.export_tap(self.neg.net, self.wrapper.neg) + + +class TestWrapperPassiveCircuit(DesignTop): + @override + def contents(self) -> None: + super().contents() + + self.source = self.Block(TestFakeSource()) + self.sink = self.Block(SinkWrapperPassiveBlock()) + + self.vpos = self.connect(self.source.pos, self.sink.pos) + self.gnd = self.connect(self.source.neg, self.sink.neg) + + +class NetlistWrapperTestCase(unittest.TestCase): + def test_wrapper_netlist(self) -> None: + net = NetlistTestCase.generate_single_net(TestWrapperCircuit) + + self.assertIn( + NetBlock( + "Inductor_SMD:L_0603_1608Metric", + "L1", + "", + "100", + ["sink", "wrapper"], + [ + "edg.electronics_model.test_netlist_subboard_wrapper.SinkWrapperBlock", + "edg.electronics_model.test_netlist_subboard_wrapper.SinkWrapperExterior", + ], + ), + net.blocks, + ) + self.assertEqual(len(net.blocks), 2) # should only generate top-level source and sink + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink", "wrapper"], "1")], # ensure extraneous nets not generated + [ + TransformUtil.Path.empty().append_block("source").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink", "model").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("pos", "net"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink", "wrapper"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink", "model").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("neg", "net"), + ], + ), + net.nets, + ) + self.assertEqual(len(net.nets), 2) # ensure empty nets pruned + + def test_wrapper_passive_netlist(self) -> None: + net = NetlistTestCase.generate_single_net(TestWrapperPassiveCircuit) + + self.assertIn( + NetBlock( + "Inductor_SMD:L_0603_1608Metric", + "L1", + "", + "100", + ["sink", "wrapper"], + [ + "edg.electronics_model.test_netlist_subboard_wrapper.SinkWrapperPassiveBlock", + "edg.electronics_model.test_netlist_subboard_wrapper.SinkWrapperPassiveExterior", + ], + ), + net.blocks, + ) + self.assertEqual(len(net.blocks), 2) # should only generate top-level source and sink + + self.assertIn( + Net( + "vpos", + [NetPin(["source"], "1"), NetPin(["sink", "wrapper"], "1")], # ensure extraneous nets not generated + [ + TransformUtil.Path.empty().append_block("source").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink", "model").append_port("pos", "net"), + TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("pos"), + ], + ), + net.nets, + ) + self.assertIn( + Net( + "gnd", + [NetPin(["source"], "2"), NetPin(["sink", "wrapper"], "2")], + [ + TransformUtil.Path.empty().append_block("source").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink", "model").append_port("neg", "net"), + TransformUtil.Path.empty().append_block("sink", "wrapper").append_port("neg"), + ], + ), + net.nets, + ) + self.assertEqual(len(net.nets), 2) # ensure empty nets pruned