Skip to content

Commit a0bf421

Browse files
authored
Add netlists and fix layout templates for svbpcb backend (#414)
- Change placement data structure to not require name on elements, storing elements as a list - Add a blackbox block for arrange_blocks, used to place layout templates - Add _svgpcb_bbox to the template API, to return a bounding box to be used in initial placement - Refactors svgpcb template APIs to assert instead of returning optional, in most cases those errors cannot be handled
1 parent 9c510dc commit a0bf421

File tree

54 files changed

+11234
-6250
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+11234
-6250
lines changed

edg/BoardCompiler.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]
1919

2020
design_filename = os.path.join(target_dir, f'{target_name}.edg')
2121
netlist_filename = os.path.join(target_dir, f'{target_name}.net')
22-
netlist_refdes_filename = os.path.join(target_dir, f'{target_name}.ref.net')
2322
bom_filename = os.path.join(target_dir, f'{target_name}.csv')
2423
svgpcb_filename = os.path.join(target_dir, f'{target_name}.svgpcb.js')
2524

2625
with suppress(FileNotFoundError):
2726
os.remove(design_filename)
2827
with suppress(FileNotFoundError):
2928
os.remove(netlist_filename)
30-
with suppress(FileNotFoundError):
31-
os.remove(netlist_refdes_filename)
3229
with suppress(FileNotFoundError):
3330
os.remove(bom_filename)
3431
with suppress(FileNotFoundError):

edg/electronics_model/SvgPcbBackend.py

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,53 +14,76 @@ class PlacedBlock(NamedTuple):
1414
"""A placement of a hierarchical block, including the coordinates of its immediate elements.
1515
Elements are placed in local space, with (0, 0) as the origin and elements moved as a group.
1616
Elements are indexed by name."""
17-
elts: Dict[str, Tuple[Union['PlacedBlock', TransformUtil.Path], Tuple[float, float]]] # name -> elt, (x, y)
17+
elts: List[Tuple[Union['PlacedBlock', TransformUtil.Path], Tuple[float, float]]] # name -> elt, (x, y)
1818
height: float
1919
width: float
2020

2121

22-
def arrange_netlist(netlist: Netlist) -> PlacedBlock:
22+
class BlackBoxBlock(NamedTuple):
23+
path: TransformUtil.Path
24+
bbox: Tuple[float, float, float, float]
25+
26+
27+
def arrange_blocks(blocks: List[NetBlock],
28+
additional_blocks: List[BlackBoxBlock] = []) -> PlacedBlock:
2329
FOOTPRINT_BORDER = 1 # mm
2430
BLOCK_BORDER = 2 # mm
2531

2632
# create list of blocks by path
27-
block_subblocks: Dict[Tuple[str, ...], Set[str]] = {}
28-
block_footprints: Dict[Tuple[str, ...], List[NetBlock]] = {}
33+
block_subblocks: Dict[Tuple[str, ...], List[str]] = {} # list to maintain sortedness
34+
block_footprints: Dict[Tuple[str, ...], List[Union[NetBlock, BlackBoxBlock]]] = {}
2935

3036
# for here, we only group one level deep
31-
for block in netlist.blocks:
37+
for block in blocks:
3238
containing_path = block.full_path.blocks[0:min(len(block.full_path.blocks) - 1, 1)]
3339
block_footprints.setdefault(containing_path, []).append(block)
3440
for i in range(len(containing_path)):
35-
block_subblocks.setdefault(tuple(containing_path[:i]), set()).add(containing_path[i])
41+
subblocks_list = block_subblocks.setdefault(tuple(containing_path[:i]), list())
42+
if containing_path[i] not in subblocks_list:
43+
subblocks_list.append(containing_path[i])
44+
45+
for blackbox in additional_blocks:
46+
containing_path = blackbox.path.blocks[0:min(len(blackbox.path.blocks) - 1, 1)]
47+
block_footprints.setdefault(containing_path, []).append(blackbox)
48+
for i in range(len(containing_path)):
49+
subblocks_list = block_subblocks.setdefault(tuple(containing_path[:i]), list())
50+
if containing_path[i] not in subblocks_list:
51+
subblocks_list.append(containing_path[i])
3652

3753
def arrange_hierarchy(root: Tuple[str, ...]) -> PlacedBlock:
3854
"""Recursively arranges the immediate components of a hierarchy, treating each element
3955
as a bounding box rectangle, and trying to maintain some aspect ratio."""
4056
# TODO don't count borders as part of a block's width / height
4157
ASPECT_RATIO = 16 / 9
4258

43-
sub_placed: List[Tuple[str, float, float, Union[PlacedBlock, NetBlock]]] = [] # (name, width, height, PlacedBlock or footprint)
44-
for subblock in block_subblocks.get(root, set()):
59+
sub_placed: List[Tuple[float, float, Union[PlacedBlock, NetBlock, BlackBoxBlock]]] = [] # (width, height, entry)
60+
for subblock in block_subblocks.get(root, list()):
4561
subplaced = arrange_hierarchy(root + (subblock,))
46-
sub_placed.append((subblock, subplaced.width + BLOCK_BORDER, subplaced.height + BLOCK_BORDER, subplaced))
62+
sub_placed.append((subplaced.width + BLOCK_BORDER, subplaced.height + BLOCK_BORDER, subplaced))
4763

4864
for footprint in block_footprints.get(root, []):
49-
bbox = FootprintDataTable.bbox_of(footprint.footprint) or (1, 1, 1, 1)
65+
if isinstance(footprint, NetBlock):
66+
bbox = FootprintDataTable.bbox_of(footprint.footprint) or (1, 1, 1, 1)
67+
entry: Union[PlacedBlock, NetBlock, BlackBoxBlock] = footprint
68+
elif isinstance(footprint, BlackBoxBlock):
69+
bbox = footprint.bbox
70+
entry = footprint
71+
else:
72+
raise TypeError()
5073
width = bbox[2] - bbox[0] + FOOTPRINT_BORDER
5174
height = bbox[3] - bbox[1] + FOOTPRINT_BORDER
5275
# use refdes as key so it's globally unique, for when this is run with blocks grouped together
53-
sub_placed.append((footprint.refdes, width, height, footprint))
76+
sub_placed.append((width, height, entry))
5477

55-
total_area = sum(width * height for _, width, height, _ in sub_placed)
78+
total_area = sum(width * height for width, height, _ in sub_placed)
5679
max_width = math.sqrt(total_area * ASPECT_RATIO)
5780

5881
x_max = 0.0
5982
y_max = 0.0
6083
# track the y limits and y position of the prior elements
6184
x_stack: List[Tuple[float, float, float]] = [] # [(x pos of next, y pos, y limit)]
62-
elts: Dict[str, Tuple[Union[PlacedBlock, TransformUtil.Path], Tuple[float, float]]] = {}
63-
for name, width, height, entry in sorted(sub_placed, key=lambda x: -x[2]): # by height
85+
elts: List[Tuple[Union[PlacedBlock, TransformUtil.Path], Tuple[float, float]]] = []
86+
for width, height, entry in sorted(sub_placed, key=lambda x: -x[1]): # by height
6487
if not x_stack: # only on first component
6588
next_y = 0.0
6689
else:
@@ -83,10 +106,13 @@ def arrange_hierarchy(root: Tuple[str, ...]) -> PlacedBlock:
83106
next_x = x_stack[-1][0]
84107

85108
if isinstance(entry, PlacedBlock): # assumed (0, 0) at top left
86-
elts[name] = (entry, (next_x, next_y))
109+
elts.append((entry, (next_x, next_y)))
87110
elif isinstance(entry, NetBlock): # account for footprint origin, flipping y-axis
88111
bbox = FootprintDataTable.bbox_of(entry.footprint) or (0, 0, 0, 0)
89-
elts[name] = (entry.full_path, (next_x - bbox[0], next_y + bbox[3]))
112+
elts.append((entry.full_path, (next_x - bbox[0], next_y + bbox[3])))
113+
elif isinstance(entry, BlackBoxBlock): # account for footprint origin, flipping y-axis
114+
bbox = entry.bbox
115+
elts.append((entry.path, (next_x - bbox[0], next_y - bbox[0])))
90116
x_stack.append((next_x + width, next_y, next_y + height))
91117
x_max = max(x_max, next_x + width)
92118
y_max = max(y_max, next_y + height)
@@ -100,7 +126,7 @@ def flatten_packed_block(block: PlacedBlock) -> Dict[TransformUtil.Path, Tuple[f
100126
"""Flatten a packed_block to a dict of individual components."""
101127
flattened: Dict[TransformUtil.Path, Tuple[float, float]] = {}
102128
def walk_group(block: PlacedBlock, x_pos: float, y_pos: float) -> None:
103-
for _, (elt, (elt_x, elt_y)) in block.elts.items():
129+
for elt, (elt_x, elt_y) in block.elts:
104130
if isinstance(elt, PlacedBlock):
105131
walk_group(elt, x_pos + elt_x, y_pos + elt_y)
106132
elif isinstance(elt, TransformUtil.Path):
@@ -115,6 +141,7 @@ class SvgPcbGeneratedBlock(NamedTuple):
115141
path: TransformUtil.Path
116142
fn_name: str
117143
svgpcb_code: str
144+
bbox: Tuple[float, float, float, float]
118145

119146

120147
class SvgPcbTransform(TransformUtil.Transform):
@@ -138,7 +165,7 @@ def visit_block(self, context: TransformUtil.TransformContext, block: edgir.Bloc
138165
generator_obj = cls()
139166
generator_obj._svgpcb_init(context.path, self.design, self.netlist)
140167
self._svgpcb_blocks.append(SvgPcbGeneratedBlock(
141-
context.path, generator_obj._svgpcb_fn_name(), generator_obj._svgpcb_template()
168+
context.path, generator_obj._svgpcb_fn_name(), generator_obj._svgpcb_template(), generator_obj._svgpcb_bbox()
142169
))
143170
else:
144171
pass
@@ -168,35 +195,51 @@ def filter_blocks_by_pathname(blocks: List[NetBlock], exclude_prefixes: List[Tup
168195
return [block for block in blocks
169196
if not block_matches_prefixes(block, exclude_prefixes)]
170197

198+
# handle blocks with svgpcb templates
171199
svgpcb_blocks = SvgPcbTransform(design, netlist).run()
172-
svgpcb_block_prefixes = [block.path.to_tuple() for block in svgpcb_blocks]
200+
svgpcb_block_bboxes = [BlackBoxBlock(block.path, block.bbox) for block in svgpcb_blocks]
201+
202+
# handle footprints
173203
netlist = NetlistTransform(design).run()
204+
svgpcb_block_prefixes = [block.path.to_tuple() for block in svgpcb_blocks]
174205
other_blocks = filter_blocks_by_pathname(netlist.blocks, svgpcb_block_prefixes)
175-
arranged_blocks = arrange_netlist(netlist)
206+
arranged_blocks = arrange_blocks(other_blocks, svgpcb_block_bboxes)
176207
pos_dict = flatten_packed_block(arranged_blocks)
177208

178-
svgpcb_block_instantiations = [
179-
f"const {SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(block.path)} = {block.fn_name}(pt(0, 0))"
180-
for block in svgpcb_blocks
181-
]
209+
# note, dimensions in inches, divide by 25.4 to convert from mm
210+
svgpcb_block_instantiations = []
211+
for svgpcb_block in svgpcb_blocks:
212+
x_pos, y_pos = pos_dict.get(svgpcb_block.path, (0, 0)) # in mm, need to convert to in below
213+
block_code = f"const {SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(svgpcb_block.path)} = {svgpcb_block.fn_name}(pt({x_pos/25.4:.3f}, {y_pos/25.4:.3f}))"
214+
svgpcb_block_instantiations.append(block_code)
182215

183-
# note, dimensions in inches
184216
other_block_instantiations = []
185-
for block in other_blocks:
186-
x_pos, y_pos = pos_dict.get(block.full_path, (0, 0)) # in mm, need to convert to in below
217+
for net_block in other_blocks:
218+
x_pos, y_pos = pos_dict.get(net_block.full_path, (0, 0)) # in mm, need to convert to in below
187219
block_code = f"""\
188-
const {SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(block.full_path)} = board.add({SvgPcbTemplateBlock._svgpcb_footprint_to_svgpcb(block.footprint)}, {{
220+
// {net_block.full_path}
221+
const {net_block.refdes} = board.add({SvgPcbTemplateBlock._svgpcb_footprint_to_svgpcb(net_block.footprint)}, {{
189222
translate: pt({x_pos/25.4:.3f}, {y_pos/25.4:.3f}), rotate: 0,
190-
id: '{SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(block.full_path)}'
223+
id: '{net_block.refdes}'
191224
}})"""
192225
other_block_instantiations.append(block_code)
193226

227+
net_blocks_by_path = {net_block.full_path: net_block for net_block in netlist.blocks}
228+
netlist_code_entries = []
229+
for net in netlist.nets:
230+
pads_code = [f"""["{net_blocks_by_path[pin.block_path].refdes}", "{pin.pin_name}"]""" for pin in net.pins]
231+
netlist_code_entries.append(f"""{{name: "{net.name}", pads: [{', '.join(pads_code)}]}}""")
232+
194233
NEWLINE = '\n'
195234
full_code = f"""\
196235
const board = new PCB();
197236
198237
{NEWLINE.join(svgpcb_block_instantiations + other_block_instantiations)}
199238
239+
board.setNetlist([
240+
{("," + NEWLINE + " ").join(netlist_code_entries)}
241+
])
242+
200243
const limit0 = pt(-{2/25.4}, -{2/25.4});
201244
const limit1 = pt({arranged_blocks.width/25.4}, {arranged_blocks.height/25.4});
202245
const xMin = Math.min(limit0[0], limit1[0]);

edg/electronics_model/SvgPcbTemplateBlock.py

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional, Any, List
1+
from typing import Optional, Any, List, Tuple
22

33
from abc import abstractmethod
44
from ..core import *
@@ -33,26 +33,38 @@ def _svgpcb_pathname(self) -> str:
3333
"""Infrastructure method, returns the pathname for this Block as a JS-code-friendly string."""
3434
return self._svgpcb_pathname_to_svgpcb(self._svgpcb_pathname_data)
3535

36-
def _svgpcb_get(self, param: ConstraintExpr[Any, Any]) -> Optional[str]:
37-
"""Infrastructure method, returns the value of the ConstraintExpr as a JS literal.
38-
Ranges are mapped to a two-element list."""
36+
def _svgpcb_get(self, param: ConstraintExpr[Any, Any]) -> Any:
37+
"""Infrastructure method, returns the value of the ConstraintExpr. Asserts out if the value isn't available"""
3938
param_path = self._svgpcb_ref_map.get(param, None)
40-
if param_path is None:
41-
return None
39+
assert param_path is not None
4240
param_val = self._svgpcb_design.get_value(param_path)
43-
if param_val is None:
44-
return None
45-
# TODO structure the output to be JS-friendly
46-
return str(param_val)
47-
48-
def _svgpcb_footprint_block_path_of(self, block_ref: List[str]) -> Optional[TransformUtil.Path]:
49-
"""Infrastructure method, given the name of a container block, returns the block path of the footprint block
50-
if there is exactly one. Otherwise, returns None."""
41+
assert param_val is not None
42+
return param_val
43+
44+
def _svgpcb_refdes_of(self, block_ref: List[str]) -> Tuple[str, int]:
45+
"""Returns the refdes of a block, as a tuple of prefix and number,
46+
or crashes if the block is not valid."""
5147
block_path = self._svgpcb_pathname_data.append_block(*block_ref)
5248
candidate_blocks = [block for block in self._svgpcb_netlist.blocks
5349
if block.full_path.startswith(block_path)]
54-
if len(candidate_blocks) != 1:
55-
return None
50+
assert len(candidate_blocks) == 1
51+
refdes = candidate_blocks[0].refdes
52+
assert isinstance(refdes, str)
53+
assert refdes is not None
54+
for i in reversed(range(len(refdes))): # split between letter and numeric parts
55+
if refdes[i].isalpha():
56+
if i == len(refdes) - 1:
57+
return refdes, -1 # fallback if no numeric portion
58+
return refdes[:i+1], int(refdes[i+1:])
59+
return "", int(refdes)
60+
61+
def _svgpcb_footprint_block_path_of(self, block_ref: List[str]) -> TransformUtil.Path:
62+
"""Infrastructure method, given the name of a container block, returns the block path of the footprint block.
63+
Asserts there is exactly one."""
64+
block_path = self._svgpcb_pathname_data.append_block(*block_ref)
65+
candidate_blocks = [block for block in self._svgpcb_netlist.blocks
66+
if block.full_path.startswith(block_path)]
67+
assert len(candidate_blocks) == 1
5668
return candidate_blocks[0].full_path
5769

5870
def _svgpcb_footprint_of(self, path: TransformUtil.Path) -> str:
@@ -63,18 +75,17 @@ def _svgpcb_footprint_of(self, path: TransformUtil.Path) -> str:
6375
assert len(candidate_blocks) == 1
6476
return self._svgpcb_footprint_to_svgpcb(candidate_blocks[0].footprint)
6577

66-
def _svgpcb_pin_of(self, block_ref: List[str], pin_ref: List[str], footprint_path: TransformUtil.Path) -> Optional[str]:
78+
def _svgpcb_pin_of(self, block_ref: List[str], pin_ref: List[str]) -> str:
6779
"""Infrastructure method, given a footprint path from _svgpcb_footprint_block_path_of and a port that should
68-
be connected to one of its pins, returns the footprint pin that the port is connected to, if any."""
69-
port_path = self._svgpcb_pathname_data.append_block(*block_ref).append_port(*pin_ref)
80+
be connected to one of its pins, returns the footprint pin that the port is connected to."""
81+
footprint_path = self._svgpcb_footprint_block_path_of(block_ref)
82+
port_path = footprint_path.append_port(*pin_ref)
7083
candidate_nets = [net for net in self._svgpcb_netlist.nets
7184
if port_path in net.ports]
72-
if len(candidate_nets) != 1:
73-
return None
85+
assert len(candidate_nets) == 1
7486
candidate_pins = [pin for pin in candidate_nets[0].pins
7587
if pin.block_path == footprint_path]
76-
if len(candidate_pins) != 1:
77-
return None
88+
assert len(candidate_pins) == 1
7889
return candidate_pins[0].pin_name
7990

8091
def _svgpcb_fn_name_adds(self) -> Optional[str]:
@@ -90,6 +101,10 @@ def _svgpcb_fn_name(self) -> str:
90101
else:
91102
return f"""{self.__class__.__name__}_{self._svgpcb_pathname()}"""
92103

104+
def _svgpcb_bbox(self) -> Tuple[float, float, float, float]:
105+
"""Returns the bounding box (xmin, ymin, xmax, ymax) in mm of the svgpcb layout with default parameters."""
106+
return 0.0, 0.0, 1.0, 1.0
107+
93108
@abstractmethod
94109
def _svgpcb_template(self) -> str:
95110
"""IMPLEMENT ME. Returns the SVGPCB layout template code as JS function named _svgpcb_fn_name.

0 commit comments

Comments
 (0)