Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrapper Footprint Blocks #385

Merged
merged 24 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions edg/electronics_model/CircuitBlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ def footprint(self, refdes: StringLike, footprint: StringLike, pinning: Mapping[
self.assign(self.fp_datasheet, '')


class WrapperFootprintBlock(FootprintBlock):
"""Block that has a footprint and optional internal contents, but the netlister ignores internal components.
Useful for, for example, a breakout board where the modelling details are provided by internal chip blocks,
but needs to show up as only a carrier board footprint.
EXPERIMENTAL - API SUBJECT TO CHANGE."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fp_is_wrapper = self.Metadata("A") # TODO replace with not metadata, eg superclass inspection


@abstract_block
class NetBlock(InternalBlock, NetBaseBlock, Block):
def contents(self):
Expand Down
150 changes: 94 additions & 56 deletions edg/electronics_model/NetlistGenerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,23 @@ class Netlist(NamedTuple):
nets: List[Net]


Blocks = Dict[TransformUtil.Path, NetBlock] # Path -> Block
Edges = Dict[TransformUtil.Path, List[TransformUtil.Path]] # Port Path -> connected Port Paths
AssertConnected = List[Tuple[TransformUtil.Path, TransformUtil.Path]]
# The concept of board scopes is footprints associated with a scope (defined as a path) that is a board,
# to support multi-board assemblies and virtual components.
# The default (top-level) board has TransformUtil.Path.empty().
# None board scope means the footprints do not exist (virtual component), eg the associated blocks were for modeling.
class BoardScope(NamedTuple):
path: TransformUtil.Path # root path
footprints: Dict[TransformUtil.Path, NetBlock] # Path -> Block footprint
edges: Dict[TransformUtil.Path, List[TransformUtil.Path]] # Port Path -> connected Port Paths
pins: Dict[TransformUtil.Path, List[NetPin]] # mapping from Port to pad
assert_connected: List[Tuple[TransformUtil.Path, TransformUtil.Path]]

@classmethod
def empty(cls, path: TransformUtil.Path): # returns a fresh, empty BordScope
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
class NetlistTransform(TransformUtil.Transform):
@staticmethod
Expand All @@ -53,28 +67,38 @@ def flatten_port(path: TransformUtil.Path, port: edgir.PortLike) -> Iterable[Tra
raise ValueError(f"don't know how to flatten netlistable port {port}")

def __init__(self, design: CompiledDesign):
self.blocks: Blocks = {}
self.edges: Edges = {} # as port Paths, including intermediates
self.pins: Dict[TransformUtil.Path, List[NetPin]] = {} # mapping from Port to pad
self.assert_connected: AssertConnected = []
self.all_scopes = [BoardScope.empty(TransformUtil.Path.empty())] # list of unique scopes
self.scopes: Scopes = {TransformUtil.Path.empty(): self.all_scopes[0]}

self.short_paths: Dict[TransformUtil.Path, List[str]] = {TransformUtil.Path.empty(): []} # seed root
self.class_paths: ClassPaths = {TransformUtil.Path.empty(): []} # seed root

self.design = design

def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, edgir.LinkArray, edgir.HierarchyBlock]) -> None:
# TODO may need rethought to support multi-board assemblies
scope = self.scopes[path] # including footprint and exports, and everything within a link
internal_scope = scope # for internal blocks

if isinstance(block, edgir.HierarchyBlock):
if 'fp_is_wrapper' in block.meta.members.node: # wrapper internal blocks ignored
internal_scope = None
for block_pair in block.blocks:
self.scopes[path.append_block(block_pair.name)] = internal_scope
for link_pair in block.links: # links considered to be the same scope as self
self.scopes[path.append_link(link_pair.name)] = scope

# generate short paths for children first, for Blocks only
main_internal_blocks: Dict[str, edgir.BlockLike] = {}
other_internal_blocks: Dict[str, edgir.BlockLike] = {}
if isinstance(block, edgir.HierarchyBlock):
for block_pair in block.blocks:
subblock = block_pair.value
# ignore pseudoblocks like bridges and adapters that have no internals
if not subblock.hierarchy.blocks and 'fp_is_footprint' not in subblock.hierarchy.meta.members.node:
other_internal_blocks[block_pair.name] = block_pair.value
else:
main_internal_blocks[block_pair.name] = block_pair.value

for block_pair in block.blocks:
subblock = block_pair.value
# ignore pseudoblocks like bridges and adapters that have no internals
if not subblock.hierarchy.blocks and 'fp_is_footprint' not in subblock.hierarchy.meta.members.node:
other_internal_blocks[block_pair.name] = block_pair.value
else:
main_internal_blocks[block_pair.name] = block_pair.value

short_path = self.short_paths[path]
class_path = self.class_paths[path]
Expand All @@ -91,17 +115,20 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
for (name, subblock) in other_internal_blocks.items():
self.short_paths[path.append_block(name)] = short_path + [name]
self.class_paths[path.append_block(name)] = class_path + [subblock.hierarchy.self_class]
elif isinstance(block, (edgir.Link, edgir.LinkArray)):
for link_pair in block.links:
self.scopes[path.append_link(link_pair.name)] = scope

if 'nets' in block.meta.members.node:
if 'nets' in block.meta.members.node and scope is not None:
# add self as a net
# list conversion to deal with iterable-once
flat_ports = list(chain(*[self.flatten_port(path.append_port(port_pair.name), port_pair.value)
for port_pair in block.ports]))
self.edges.setdefault(path, []).extend(flat_ports)
scope.edges.setdefault(path, []).extend(flat_ports)
for port_path in flat_ports:
self.edges.setdefault(port_path, []).append(path)
scope.edges.setdefault(port_path, []).append(path)

if 'nets_packed' in block.meta.members.node:
if 'nets_packed' in block.meta.members.node and scope is not None:
# this connects the first source to all destinations, then asserts all the sources are equal
# this leaves the sources unconnected, to be connected externally and checked at the end
src_port_name = block.meta.members.node['nets_packed'].members.node['src'].text_leaf
Expand All @@ -110,13 +137,13 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
flat_dsts = list(self.flatten_port(path.append_port(dst_port_name), edgir.pair_get(block.ports, dst_port_name)))
assert flat_srcs, "missing source port(s) for packed net"
for dst_path in flat_dsts:
self.edges.setdefault(flat_srcs[0], []).append(dst_path)
self.edges.setdefault(dst_path, []).append(flat_srcs[0])
scope.edges.setdefault(flat_srcs[0], []).append(dst_path)
scope.edges.setdefault(dst_path, []).append(flat_srcs[0])
for src_path in flat_srcs: # assert all sources connected
for dst_path in flat_srcs:
self.assert_connected.append((src_path, dst_path))
scope.assert_connected.append((src_path, dst_path))

if 'fp_is_footprint' in block.meta.members.node:
if 'fp_is_footprint' in block.meta.members.node and scope is not None:
footprint_name = self.design.get_value(path.to_tuple() + ('fp_footprint',))
footprint_pinning = self.design.get_value(path.to_tuple() + ('fp_pinning',))
mfr = self.design.get_value(path.to_tuple() + ('fp_mfr',))
Expand All @@ -139,14 +166,14 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
]
part_str = " ".join(filter(None, part_comps))
value_str = value if value else (part if part else '')
self.blocks[path] = NetBlock(
scope.footprints[path] = NetBlock(
footprint_name,
refdes,
part_str,
value_str,
path,
self.short_paths[path],
self.class_paths[path],
self.class_paths[path]
)

for pin_spec in footprint_pinning:
Expand All @@ -157,56 +184,63 @@ def process_blocklike(self, path: TransformUtil.Path, block: Union[edgir.Link, e
pin_port_path = edgir.LocalPathList(pin_spec_split[1].split('.'))

src_path = path.follow(pin_port_path, block)[0]
self.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named
self.pins.setdefault(src_path, []).append(NetPin(path, pin_name))
scope.edges.setdefault(src_path, []) # make sure there is a port entry so single-pin nets are named
scope.pins.setdefault(src_path, []).append(NetPin(path, pin_name))

for constraint_pair in block.constraints:
if constraint_pair.value.HasField('connected'):
self.process_connected(path, block, constraint_pair.value.connected)
elif constraint_pair.value.HasField('exported'):
self.process_exported(path, block, constraint_pair.value.exported)
elif constraint_pair.value.HasField('exportedTunnel'):
self.process_exported(path, block, constraint_pair.value.exportedTunnel)
elif constraint_pair.value.HasField('connectedArray'):
for expanded_connect in constraint_pair.value.connectedArray.expanded:
self.process_connected(path, block, expanded_connect)
elif constraint_pair.value.HasField('exportedArray'):
for expanded_export in constraint_pair.value.exportedArray.expanded:
self.process_exported(path, block, expanded_export)

def process_connected(self, path: TransformUtil.Path, current: edgir.EltTypes, constraint: edgir.ConnectedExpr) -> None:
if scope is not None:
if constraint_pair.value.HasField('connected'):
self.process_connected(path, block, scope, 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, expanded_connect)
elif constraint_pair.value.HasField('exported'):
self.process_exported(path, block, scope, 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, expanded_export)
elif constraint_pair.value.HasField('exportedTunnel'):
self.process_exported(path, block, scope, constraint_pair.value.exportedTunnel)

def process_connected(self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope,
constraint: edgir.ConnectedExpr) -> None:
if constraint.expanded:
assert len(constraint.expanded) == 1
self.process_connected(path, current, constraint.expanded[0])
self.process_connected(path, current, scope, 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))

def process_exported(self, path: TransformUtil.Path, current: edgir.EltTypes, constraint: edgir.ExportedExpr) -> None:
def process_exported(self, path: TransformUtil.Path, current: edgir.EltTypes, scope: BoardScope,
constraint: edgir.ExportedExpr) -> None:
if constraint.expanded:
assert len(constraint.expanded) == 1
self.process_exported(path, current, constraint.expanded[0])
self.process_exported(path, current, scope, constraint.expanded[0])
return
assert constraint.internal_block_port.HasField('ref')
assert constraint.exterior_port.HasField('ref')
self.connect_ports(
scope,
path.follow(constraint.internal_block_port.ref, current),
path.follow(constraint.exterior_port.ref, current))

def connect_ports(self, elt1: Tuple[TransformUtil.Path, edgir.EltTypes], elt2: Tuple[TransformUtil.Path, edgir.EltTypes]) -> None:
def connect_ports(self, scope: BoardScope, elt1: Tuple[TransformUtil.Path, edgir.EltTypes],
elt2: Tuple[TransformUtil.Path, edgir.EltTypes]) -> None:
"""Recursively connect ports as applicable"""
if isinstance(elt1[1], edgir.Port) and isinstance(elt2[1], edgir.Port):
self.edges.setdefault(elt1[0], []).append(elt2[0])
self.edges.setdefault(elt2[0], []).append(elt1[0])
scope.edges.setdefault(elt1[0], []).append(elt2[0])
scope.edges.setdefault(elt2[0], []).append(elt1[0])
elif isinstance(elt1[1], edgir.Bundle) and isinstance(elt2[1], edgir.Bundle):
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 bundle types {elt1}, {elt2}"
for key in elt2_names:
self.connect_ports(
scope,
(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))))
# don't need to create the bundle connect, since Bundles can't be CircuitPorts
Expand Down Expand Up @@ -255,14 +289,12 @@ def pin_name_goodness(pin1: TransformUtil.Path, pin2: TransformUtil.Path) -> int

return net_prefix + str(best_path)

def run(self) -> Netlist:
self.transform_design(self.design.design)

def scope_to_netlist(self, scope: BoardScope) -> Netlist:
# Convert to the netlist format
seen: Set[TransformUtil.Path] = set()
nets: List[List[TransformUtil.Path]] = [] # lists preserve ordering

for port, conns in self.edges.items():
for port, conns in scope.edges.items():
if port not in seen:
curr_net: List[TransformUtil.Path] = []
frontier: List[TransformUtil.Path] = [port] # use BFS to maintain ordering instead of simpler DFS
Expand All @@ -271,15 +303,15 @@ def run(self) -> Netlist:
if pin not in seen:
seen.add(pin)
curr_net.append(pin)
frontier.extend(self.edges[pin])
frontier.extend(scope.edges[pin])
nets.append(curr_net)

pin_to_net: Dict[TransformUtil.Path, List[TransformUtil.Path]] = {} # values share reference to nets
for net in nets:
for pin in net:
pin_to_net[pin] = net

for (connected1, connected2) in self.assert_connected:
for (connected1, connected2) in scope.assert_connected:
if pin_to_net[connected1] is not pin_to_net[connected2]:
raise InvalidPackingException(f"packed pins {connected1}, {connected2} not connected")

Expand All @@ -294,10 +326,16 @@ def run(self) -> Netlist:
def port_ignored_paths(path: TransformUtil.Path) -> bool: # ignore link ports for netlisting
return bool(path.links) or any([block.startswith('(adapter)') or block.startswith('(bridge)') for block in path.blocks])

netlist_blocks = [block for path, block in self.blocks.items()]
netlist_footprints = [footprint for path, footprint in scope.footprints.items()]
netlist_nets = [Net(name,
list(chain(*[self.pins[port] for port in net if port in self.pins])),
list(chain(*[scope.pins[port] for port in net if port in scope.pins])),
[port for port in net if not port_ignored_paths(port)])
for name, net in named_nets.items()]
netlist_nets = [net for net in netlist_nets if net.pins] # prune empty nets

return Netlist(netlist_footprints, netlist_nets)

def run(self) -> Netlist:
self.transform_design(self.design.design)

return Netlist(netlist_blocks, netlist_nets)
return self.scope_to_netlist(self.all_scopes[0]) # TODO support multiple scopes
24 changes: 17 additions & 7 deletions edg/electronics_model/RefdesRefinementPass.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Tuple, Dict, Set
from typing import List, Tuple, Dict, Set, Optional

from .. import edgir
from ..core import CompiledDesign, TransformUtil
Expand All @@ -23,18 +23,28 @@ def __init__(self, design: CompiledDesign):
assert isinstance(board_refdes_prefix, str)
self.board_refdes_prefix = board_refdes_prefix

self.scopes: Dict[TransformUtil.Path, Optional[TransformUtil.Path]] = {
TransformUtil.Path.empty(): TransformUtil.Path.empty()
}

self.block_refdes_list: List[Tuple[TransformUtil.Path, str]] = [] # populated in traversal order
self.seen_blocks: Set[TransformUtil.Path] = set()
self.refdes_last: Dict[str, int] = {}
self.refdes_last: Dict[Tuple[TransformUtil.Path, str], int] = {} # (scope, prefix) -> num

def visit_block(self, context: TransformUtil.TransformContext, block: edgir.BlockTypes) -> None:
if 'fp_is_footprint' in block.meta.members.node:
scope = self.scopes[context.path]
internal_scope = scope
if 'fp_is_wrapper' in block.meta.members.node: # wrapper internal blocks ignored
internal_scope = None

for block_pair in block.blocks:
self.scopes[context.path.append_block(block_pair.name)] = internal_scope

if 'fp_is_footprint' in block.meta.members.node and scope is not None:
refdes_prefix = self.design.get_value(context.path.to_tuple() + ('fp_refdes_prefix',))
assert isinstance(refdes_prefix, str)

refdes_id = self.refdes_last.get(refdes_prefix, 0) + 1
self.refdes_last[refdes_prefix] = refdes_id
assert context.path not in self.seen_blocks
refdes_id = self.refdes_last.get((scope, refdes_prefix), 0) + 1
self.refdes_last[(scope, 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]]:
Expand Down
2 changes: 1 addition & 1 deletion edg/electronics_model/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from ..core import *

from .CircuitBlock import FootprintBlock, CircuitPortBridge, CircuitPortAdapter, NetBlock
from .CircuitBlock import FootprintBlock, WrapperFootprintBlock, CircuitPortBridge, CircuitPortAdapter, NetBlock
from .VoltagePorts import CircuitPort

from .Units import Farad, uFarad, nFarad, pFarad, MOhm, kOhm, Ohm, mOhm, Henry, uHenry, nHenry
Expand Down
Loading
Loading