Skip to content
Merged
3 changes: 0 additions & 3 deletions edg/BoardCompiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,13 @@ def compile_board(design: Type[Block], target_dir_name: Optional[Tuple[str, str]

design_filename = os.path.join(target_dir, f'{target_name}.edg')
netlist_filename = os.path.join(target_dir, f'{target_name}.net')
netlist_refdes_filename = os.path.join(target_dir, f'{target_name}.ref.net')
bom_filename = os.path.join(target_dir, f'{target_name}.csv')
svgpcb_filename = os.path.join(target_dir, f'{target_name}.svgpcb.js')

with suppress(FileNotFoundError):
os.remove(design_filename)
with suppress(FileNotFoundError):
os.remove(netlist_filename)
with suppress(FileNotFoundError):
os.remove(netlist_refdes_filename)
with suppress(FileNotFoundError):
os.remove(bom_filename)
with suppress(FileNotFoundError):
Expand Down
101 changes: 72 additions & 29 deletions edg/electronics_model/SvgPcbBackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,76 @@ class PlacedBlock(NamedTuple):
"""A placement of a hierarchical block, including the coordinates of its immediate elements.
Elements are placed in local space, with (0, 0) as the origin and elements moved as a group.
Elements are indexed by name."""
elts: Dict[str, Tuple[Union['PlacedBlock', TransformUtil.Path], Tuple[float, float]]] # name -> elt, (x, y)
elts: List[Tuple[Union['PlacedBlock', TransformUtil.Path], Tuple[float, float]]] # name -> elt, (x, y)
height: float
width: float


def arrange_netlist(netlist: Netlist) -> PlacedBlock:
class BlackBoxBlock(NamedTuple):
path: TransformUtil.Path
bbox: Tuple[float, float, float, float]


def arrange_blocks(blocks: List[NetBlock],
additional_blocks: List[BlackBoxBlock] = []) -> PlacedBlock:
FOOTPRINT_BORDER = 1 # mm
BLOCK_BORDER = 2 # mm

# create list of blocks by path
block_subblocks: Dict[Tuple[str, ...], Set[str]] = {}
block_footprints: Dict[Tuple[str, ...], List[NetBlock]] = {}
block_subblocks: Dict[Tuple[str, ...], List[str]] = {} # list to maintain sortedness
block_footprints: Dict[Tuple[str, ...], List[Union[NetBlock, BlackBoxBlock]]] = {}

# for here, we only group one level deep
for block in netlist.blocks:
for block in blocks:
containing_path = block.full_path.blocks[0:min(len(block.full_path.blocks) - 1, 1)]
block_footprints.setdefault(containing_path, []).append(block)
for i in range(len(containing_path)):
block_subblocks.setdefault(tuple(containing_path[:i]), set()).add(containing_path[i])
subblocks_list = block_subblocks.setdefault(tuple(containing_path[:i]), list())
if containing_path[i] not in subblocks_list:
subblocks_list.append(containing_path[i])

for blackbox in additional_blocks:
containing_path = blackbox.path.blocks[0:min(len(blackbox.path.blocks) - 1, 1)]
block_footprints.setdefault(containing_path, []).append(blackbox)
for i in range(len(containing_path)):
subblocks_list = block_subblocks.setdefault(tuple(containing_path[:i]), list())
if containing_path[i] not in subblocks_list:
subblocks_list.append(containing_path[i])

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

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

for footprint in block_footprints.get(root, []):
bbox = FootprintDataTable.bbox_of(footprint.footprint) or (1, 1, 1, 1)
if isinstance(footprint, NetBlock):
bbox = FootprintDataTable.bbox_of(footprint.footprint) or (1, 1, 1, 1)
entry: Union[PlacedBlock, NetBlock, BlackBoxBlock] = footprint
elif isinstance(footprint, BlackBoxBlock):
bbox = footprint.bbox
entry = footprint
else:
raise TypeError()
width = bbox[2] - bbox[0] + FOOTPRINT_BORDER
height = bbox[3] - bbox[1] + FOOTPRINT_BORDER
# use refdes as key so it's globally unique, for when this is run with blocks grouped together
sub_placed.append((footprint.refdes, width, height, footprint))
sub_placed.append((width, height, entry))

total_area = sum(width * height for _, width, height, _ in sub_placed)
total_area = sum(width * height for width, height, _ in sub_placed)
max_width = math.sqrt(total_area * ASPECT_RATIO)

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

if isinstance(entry, PlacedBlock): # assumed (0, 0) at top left
elts[name] = (entry, (next_x, next_y))
elts.append((entry, (next_x, next_y)))
elif isinstance(entry, NetBlock): # account for footprint origin, flipping y-axis
bbox = FootprintDataTable.bbox_of(entry.footprint) or (0, 0, 0, 0)
elts[name] = (entry.full_path, (next_x - bbox[0], next_y + bbox[3]))
elts.append((entry.full_path, (next_x - bbox[0], next_y + bbox[3])))
elif isinstance(entry, BlackBoxBlock): # account for footprint origin, flipping y-axis
bbox = entry.bbox
elts.append((entry.path, (next_x - bbox[0], next_y - bbox[0])))
x_stack.append((next_x + width, next_y, next_y + height))
x_max = max(x_max, next_x + width)
y_max = max(y_max, next_y + height)
Expand All @@ -100,7 +126,7 @@ def flatten_packed_block(block: PlacedBlock) -> Dict[TransformUtil.Path, Tuple[f
"""Flatten a packed_block to a dict of individual components."""
flattened: Dict[TransformUtil.Path, Tuple[float, float]] = {}
def walk_group(block: PlacedBlock, x_pos: float, y_pos: float) -> None:
for _, (elt, (elt_x, elt_y)) in block.elts.items():
for elt, (elt_x, elt_y) in block.elts:
if isinstance(elt, PlacedBlock):
walk_group(elt, x_pos + elt_x, y_pos + elt_y)
elif isinstance(elt, TransformUtil.Path):
Expand All @@ -115,6 +141,7 @@ class SvgPcbGeneratedBlock(NamedTuple):
path: TransformUtil.Path
fn_name: str
svgpcb_code: str
bbox: Tuple[float, float, float, float]


class SvgPcbTransform(TransformUtil.Transform):
Expand All @@ -138,7 +165,7 @@ def visit_block(self, context: TransformUtil.TransformContext, block: edgir.Bloc
generator_obj = cls()
generator_obj._svgpcb_init(context.path, self.design, self.netlist)
self._svgpcb_blocks.append(SvgPcbGeneratedBlock(
context.path, generator_obj._svgpcb_fn_name(), generator_obj._svgpcb_template()
context.path, generator_obj._svgpcb_fn_name(), generator_obj._svgpcb_template(), generator_obj._svgpcb_bbox()
))
else:
pass
Expand Down Expand Up @@ -168,35 +195,51 @@ def filter_blocks_by_pathname(blocks: List[NetBlock], exclude_prefixes: List[Tup
return [block for block in blocks
if not block_matches_prefixes(block, exclude_prefixes)]

# handle blocks with svgpcb templates
svgpcb_blocks = SvgPcbTransform(design, netlist).run()
svgpcb_block_prefixes = [block.path.to_tuple() for block in svgpcb_blocks]
svgpcb_block_bboxes = [BlackBoxBlock(block.path, block.bbox) for block in svgpcb_blocks]

# handle footprints
netlist = NetlistTransform(design).run()
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_netlist(netlist)
arranged_blocks = arrange_blocks(other_blocks, svgpcb_block_bboxes)
pos_dict = flatten_packed_block(arranged_blocks)

svgpcb_block_instantiations = [
f"const {SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(block.path)} = {block.fn_name}(pt(0, 0))"
for block in svgpcb_blocks
]
# note, dimensions in inches, divide by 25.4 to convert from mm
svgpcb_block_instantiations = []
for svgpcb_block in svgpcb_blocks:
x_pos, y_pos = pos_dict.get(svgpcb_block.path, (0, 0)) # in mm, need to convert to in below
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}))"
svgpcb_block_instantiations.append(block_code)

# note, dimensions in inches
other_block_instantiations = []
for block in other_blocks:
x_pos, y_pos = pos_dict.get(block.full_path, (0, 0)) # in mm, need to convert to in below
for net_block in other_blocks:
x_pos, y_pos = pos_dict.get(net_block.full_path, (0, 0)) # in mm, need to convert to in below
block_code = f"""\
const {SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(block.full_path)} = board.add({SvgPcbTemplateBlock._svgpcb_footprint_to_svgpcb(block.footprint)}, {{
// {net_block.full_path}
const {net_block.refdes} = board.add({SvgPcbTemplateBlock._svgpcb_footprint_to_svgpcb(net_block.footprint)}, {{
translate: pt({x_pos/25.4:.3f}, {y_pos/25.4:.3f}), rotate: 0,
id: '{SvgPcbTemplateBlock._svgpcb_pathname_to_svgpcb(block.full_path)}'
id: '{net_block.refdes}'
}})"""
other_block_instantiations.append(block_code)

net_blocks_by_path = {net_block.full_path: net_block for net_block in netlist.blocks}
netlist_code_entries = []
for net in netlist.nets:
pads_code = [f"""["{net_blocks_by_path[pin.block_path].refdes}", "{pin.pin_name}"]""" for pin in net.pins]
netlist_code_entries.append(f"""{{name: "{net.name}", pads: [{', '.join(pads_code)}]}}""")

NEWLINE = '\n'
full_code = f"""\
const board = new PCB();

{NEWLINE.join(svgpcb_block_instantiations + other_block_instantiations)}

board.setNetlist([
{("," + NEWLINE + " ").join(netlist_code_entries)}
])

const limit0 = pt(-{2/25.4}, -{2/25.4});
const limit1 = pt({arranged_blocks.width/25.4}, {arranged_blocks.height/25.4});
const xMin = Math.min(limit0[0], limit1[0]);
Expand Down
61 changes: 38 additions & 23 deletions edg/electronics_model/SvgPcbTemplateBlock.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Any, List
from typing import Optional, Any, List, Tuple

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

def _svgpcb_get(self, param: ConstraintExpr[Any, Any]) -> Optional[str]:
"""Infrastructure method, returns the value of the ConstraintExpr as a JS literal.
Ranges are mapped to a two-element list."""
def _svgpcb_get(self, param: ConstraintExpr[Any, Any]) -> Any:
"""Infrastructure method, returns the value of the ConstraintExpr. Asserts out if the value isn't available"""
param_path = self._svgpcb_ref_map.get(param, None)
if param_path is None:
return None
assert param_path is not None
param_val = self._svgpcb_design.get_value(param_path)
if param_val is None:
return None
# TODO structure the output to be JS-friendly
return str(param_val)

def _svgpcb_footprint_block_path_of(self, block_ref: List[str]) -> Optional[TransformUtil.Path]:
"""Infrastructure method, given the name of a container block, returns the block path of the footprint block
if there is exactly one. Otherwise, returns None."""
assert param_val is not None
return param_val

def _svgpcb_refdes_of(self, block_ref: List[str]) -> Tuple[str, int]:
"""Returns the refdes of a block, as a tuple of prefix and number,
or crashes if the block is not valid."""
block_path = self._svgpcb_pathname_data.append_block(*block_ref)
candidate_blocks = [block for block in self._svgpcb_netlist.blocks
if block.full_path.startswith(block_path)]
if len(candidate_blocks) != 1:
return None
assert len(candidate_blocks) == 1
refdes = candidate_blocks[0].refdes
assert isinstance(refdes, str)
assert refdes is not None
for i in reversed(range(len(refdes))): # split between letter and numeric parts
if refdes[i].isalpha():
if i == len(refdes) - 1:
return refdes, -1 # fallback if no numeric portion
return refdes[:i+1], int(refdes[i+1:])
return "", int(refdes)

def _svgpcb_footprint_block_path_of(self, block_ref: List[str]) -> TransformUtil.Path:
"""Infrastructure method, given the name of a container block, returns the block path of the footprint block.
Asserts there is exactly one."""
block_path = self._svgpcb_pathname_data.append_block(*block_ref)
candidate_blocks = [block for block in self._svgpcb_netlist.blocks
if block.full_path.startswith(block_path)]
assert len(candidate_blocks) == 1
return candidate_blocks[0].full_path

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

def _svgpcb_pin_of(self, block_ref: List[str], pin_ref: List[str], footprint_path: TransformUtil.Path) -> Optional[str]:
def _svgpcb_pin_of(self, block_ref: List[str], pin_ref: List[str]) -> str:
"""Infrastructure method, given a footprint path from _svgpcb_footprint_block_path_of and a port that should
be connected to one of its pins, returns the footprint pin that the port is connected to, if any."""
port_path = self._svgpcb_pathname_data.append_block(*block_ref).append_port(*pin_ref)
be connected to one of its pins, returns the footprint pin that the port is connected to."""
footprint_path = self._svgpcb_footprint_block_path_of(block_ref)
port_path = footprint_path.append_port(*pin_ref)
candidate_nets = [net for net in self._svgpcb_netlist.nets
if port_path in net.ports]
if len(candidate_nets) != 1:
return None
assert len(candidate_nets) == 1
candidate_pins = [pin for pin in candidate_nets[0].pins
if pin.block_path == footprint_path]
if len(candidate_pins) != 1:
return None
assert len(candidate_pins) == 1
return candidate_pins[0].pin_name

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

def _svgpcb_bbox(self) -> Tuple[float, float, float, float]:
"""Returns the bounding box (xmin, ymin, xmax, ymax) in mm of the svgpcb layout with default parameters."""
return 0.0, 0.0, 1.0, 1.0

@abstractmethod
def _svgpcb_template(self) -> str:
"""IMPLEMENT ME. Returns the SVGPCB layout template code as JS function named _svgpcb_fn_name.
Expand Down
Loading