Kinematic Tree#

In JITX physical layout design, objects (pins, landpatterns, components, modules, etc.) can either be placed (given a pose and a board side) in code (“code-placed”) or interactively using the graphical physical design interface. These placements are all specified with respect to the design hierarchy. Thus, a child object’s placement is relative to its parent object, and these child-parent relationships form a kinematic tree which closely follows the design hierarchy. The behavior of objects with respect to when and how they move, whether independently or as a group, depends on whether they are code-placed or interactively-placed. This gives users the freedom to choose whether a component can move freely around the board during interactive layout (interactively-placed), or to have it tied to a parent object to make it easier to move a group of components in tandem.

Code Placement#

Code-placed objects are objects whose placement is specified in code. In Python, there are two ways to specify placement:

  1. self.place(obj, position) - Places an object relative to the current circuit’s coordinate system

  2. obj.at(position) - Places an object relative to its immediate parent’s coordinate system

Circuits and components have different default placement behaviors. Circuits have a default placement of (0, 0) on Top, and are not free to move unless explicitly made floating using circuit.at(floating=True). In contrast, components are floating by default and are free to move unless explicitly placed using self.place() or .at().

Once placed, these objects will be “fixed in place” in the board view, and their placement (relative to their parent object) cannot be changed. However, a code-placed object can still potentially move if it has a parent object which is free to move and its parent is moved. Code-placed objects are grouped together based on what they are tied to. All objects which share a common ancestor module which they are “fixed” to (i.e. every module in the ancestral chain is fixed to its parent) will be part of the same group, and they will move, rotate, flip, and switch board sides together as a group.

Frame of Reference#

Poses are composed “inside-out”. That means the final position of a child object’s pose is determined by applying the pose transforms of its ancestors to its pose, in the order from closest to the child upwards to the top-level module’s pose. Thus, the pose of a child object is specified in the “frame of reference” of its parent object, i.e. the X-axis and Y-axis for the pose may be shifted, rotated, or even flipped relative to the global X-axis and Y-axis if the parent object (or some other ancestor) is shifted, rotated, or flipped.

Board Side#

The placement of objects can optionally specify a board side, either Top or Bottom. This specification is the side the object should be on assuming its parent object is placed on the Top side. If the parent object is actually placed on the Bottom side, then the child object’s board side will actually be the opposite of what is specified in the placement. Thus, placing on Top should be thought of as placing the child object on the same side as its parent, and placing on Bottom should be thought of as placing on the opposite side as the object’s parent.

You can specify the board side as an optional parameter (default is on=Side.Top):

self.place(component, (0, 0), on=Side.Top)
component.at((0, 0), on=Side.Bottom)

Interactive Placement#

Objects which do not have a code placement are free to move independently in the interactive physical layout. In other words, they are left detached from the kinematic tree. This means that even if their parent object is moved, rotated, flipped, or changes board side, they will not be affected. This applies even if the parent is code-placed.

Proxy Placement#

Control points (including single-ended control, pair control, and merge points) and non-code vias can be created interactively in the board view. These non-code-generated objects are called proxies, and they are linked to the parent component which they were created from (i.e. where the proxy was “dragged out” from). Or, if the proxy was created by dragging out from another proxy, they will share the same parent component. Proxies have the unique behavior that they will move when their parent component moves, however moving a proxy will not move its parent component. A proxy will move with its parent even if it is disconnected from its parent. See the following examples:

Proxy-only Move#

Moving the proxy does not move its parent component.

Video not Supported! Try a different browser?

Proxy Move#

Moving the parent component does move the proxy.

Video not Supported! Try a different browser?

Proxy Chain Move#

Moving the parent component moves the entire chain of proxies.

Video not Supported! Try a different browser?

Code Examples#

Placement Method Comparison#

Here’s an example showing the difference between self.place() and .at():

class Comp(Component):
    pass

class Inner(Circuit):
    comp = Comp()

class PlaceCirc(Circuit):
    inner = Inner()

    def __init__(self):
        # Using self.place() - places relative to PlaceCirc's coordinate system
        self.place(self.inner.comp, (0, 10))
        self.place(self.inner, (0, 10))
        # Result: comp will be at (0, 10) relative to PlaceCirc

class AtCirc(Circuit):
    inner = Inner()
    
    def __init__(self):
        # Using .at() - places relative to the object's immediate parent
        self.inner.comp.at(0, 10)
        # Result: comp will be at (0, 10) relative to Inner
        self.inner.at(0, 10)
        # Result: comp will be at (0, 20) relative to AtCirc
        # (10 from Inner's position + 10 from comp's position within Inner)

Hierarchy Example#

Here is example code with a hierarchy of circuits:

class CircA(Circuit):
    r1 = Resistor(resistance=50.0)
    r2 = Resistor(resistance=100.0)
    r3 = Resistor(resistance=200.0)

    def __init__(self):
        self.place(self.r1, (0.0, -2.0))
        self.place(self.r2, (0.0, 2.0))
        # r3 is not placed, so it's free to move

class CircB(Circuit):
    r4 = Resistor(resistance=500.0)
    r5 = Resistor(resistance=750.0)

    def __init__(self):
        self.place(self.r4, (-2.0, 0.0), on=Side.Bottom)
        # r4 is placed on the opposite side from CircB
        self.place(self.r5, (2.0, 0.0))

class CircC(Circuit):
    c1 = CircA()
    c2 = CircB()
    r6 = Resistor(resistance=1e3)

    def __init__(self):
        self.place(self.c1, (0.0, 0.0))
        # c2 is not placed, so it has default placement at (0, 0) on Top relative to CircC
        self.place(self.r6, (-5.0, 0.0))

class TopCirc(Circuit):
    c3 = CircC()

Based on the code-placements in the code, the following groups of instances will be tied together:

  • r1, r2, r6

    • r6 is tied to c3

    • r1 and r2 are tied to c1, which is in turn tied to c3

  • r4, r5

    • r4 and r5 are tied to c2

r3 is completely free to move independently. Note that r4 and r5 are not grouped with r1, r2, and r6 because c2 is not tied to c3.