Skip to content

Atoms

Atom

Base class for Node, Edge, and Group

Source code in model/atoms/__init__.py
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Atom:
    """Base class for Node, Edge, and Group"""
    # incrementer per type
    i = 1

    def __init__(self):
        """Atom's constructor"""
        # universally unique identifer
        self.uuid = uuid4().hex

        # default label that increments per type
        self.label = f"{self.__class__.__name__}{self.__class__.i}"
        self.__class__.i += 1

    def __str__(self):
        return self.label

__init__()

Atom's constructor

Source code in model/atoms/__init__.py
 9
10
11
12
13
14
15
16
def __init__(self):
    """Atom's constructor"""
    # universally unique identifer
    self.uuid = uuid4().hex

    # default label that increments per type
    self.label = f"{self.__class__.__name__}{self.__class__.i}"
    self.__class__.i += 1

AtomList

Bases: list

List of Atoms with additional functionality: type indexing

Source code in model/atoms/__init__.py
21
22
23
24
25
26
27
28
29
class AtomList(list):
    """List of Atoms with additional functionality: type indexing"""
    def __getitem__(self, key):
        if isinstance(key, type):
            return [item for item in self if isinstance(item, key)]
        return super().__getitem__(key)

    def __str__(self):
        return "\n".join(str(item) for item in self) + "\n"

Edge

Bases: Atom

Edge as in a graph

Source code in model/atoms/__init__.py
45
46
47
48
49
50
51
52
53
54
55
56
class Edge(Atom):
    """Edge as in a graph"""
    def __init__(self, u: Node, v: Node):
        """Edge's constructor"""
        super().__init__()

        # nodes u and v
        self.u = u
        self.v = v

    def __str__(self):
        return f"{self.label} ({str(self.u)}, {str(self.v)})"

__init__(u, v)

Edge's constructor

Source code in model/atoms/__init__.py
47
48
49
50
51
52
53
def __init__(self, u: Node, v: Node):
    """Edge's constructor"""
    super().__init__()

    # nodes u and v
    self.u = u
    self.v = v

Group

Bases: Atom

Group (wraps nodes, edges, and nested groups).

Group also includes constraints (allowed node/edge/group types) and group-specific design rules (functions that run after add/remove operations).

Source code in model/atoms/__init__.py
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
class Group(Atom):
    """
    Group (wraps nodes, edges, and nested groups).

    Group also includes constraints (allowed node/edge/group types)
    and group-specific design rules (functions that run after add/remove operations).
    """
    # allowed lists (if empty, nothing is allowed)
    allowed_nodes = []
    allowed_edges = []
    allowed_groups = []

    # design rules
    design_rules = []

    # stacking direction
    horizontal = False

    # border
    border = False

    # dunder methods
    def __init__(self, design_rules=[]):
        """Group's constructor"""
        super().__init__()

        self.design_rules = design_rules

        # absolute coordinates - calculated with calculate_pos()
        self.x = None
        self.y = None
        self.width = None
        self.height = None

        # nodes, edges, and nested groups
        self.nodes = AtomList()
        self.edges = AtomList()
        self.groups = AtomList()

    def __str__(self):
        """Returns string"""
        return f"{self.__class__.__name__} with {len(self.nodes)} nodes, {len(self.edges)} edges, and {len(self.groups)} nested groups."

    # decorators
    def enforce_allowed_types(func):
        """Decorator to enforce allowed types for primitives."""
        def wrapper(self, *args, **kwargs):
            print("Enforcing allowed types") # HERE COMES THE CHECK
            return func(self, *args, **kwargs)
        return wrapper

    def enforce_design_rules(func):
        """Decorator to enforce design rules after a primitive execution."""
        def wrapper(self, *args, **kwargs):
            result = func(self, *args, **kwargs)
            [rule() for rule in self.design_rules]
            return result
        return wrapper

    @enforce_allowed_types
    @enforce_design_rules
    def add_node(self, n: Node):
        """Adds a node to this group"""
        if not any(isinstance(n, cls) for cls in self.allowed_nodes):
            raise TypeError(f"{n.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[n.__name__ for n in self.allowed_nodes]}")
        self.nodes.append(n)

    @enforce_allowed_types
    @enforce_design_rules
    def add_edge(self, e: Edge):
        """Adds an edge to this group"""
        if not any(isinstance(e, cls) for cls in self.allowed_edges):
            raise TypeError(f"{e.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[e.__name__ for e in self.allowed_edges]}")
        self.edges.append(e)

    @enforce_allowed_types
    @enforce_design_rules
    def add_group(self, g: Group):
        """Adds a nested group to this group"""
        if not any(isinstance(g, cls) for cls in self.allowed_groups):
            raise TypeError(f"{g.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[g.__name__ for g in self.allowed_groups]}")
        self.groups.append(g)

    def add_nodes(self, ns: AtomList):
        """Adds list of nodes to this group"""
        [self.add_node(n) for n in ns]

    def add_edges(self, es: AtomList):
        """Adds list of edges to this group"""
        [self.add_edge(e) for e in es]

    def add_groups(self, gs: AtomList):
        """Adds list of nested groups to this group"""
        [self.add_group(g) for g in gs]

    def remove_node(self, n: Node):
        """Removes a node from this group"""
        if type(n) not in self.allowed_nodes:
            raise TypeError(f"{n.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[n.__name__ for n in self.allowed_nodes]}")
        self.nodes.remove(n)
        [self.remove_edge(e) for e in self.edges if e.u == n or e.v == n] # removing node also removes all associated edges

    def remove_edge(self, e: Edge):
        """Removes an edge from this group"""
        if type(e) not in self.allowed_edges:
            raise TypeError(f"{e.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[e.__name__ for e in self.allowed_edges]}")
        self.edges.remove(e)

    def remove_group(self, g: Group):
        """Removes a nested group from this group"""
        if type(g) not in self.allowed_groups:
            raise TypeError(f"{g.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[g.__name__ for g in self.allowed_groups]}")
        self.groups.remove(g)

    def remove_nodes(self, ns: list[Node]):
        """Removes a list of nodes from this group"""
        [self.remove_node(n) for n in ns]

    def remove_edges(self, es: list[Edge]):
        """Removes a list of edges from this group"""
        [self.remove_edge(e) for e in es]

    def remove_groups(self, gs: list[Group]):
        """Removes a list of groups from this group"""
        [self.remove_group(g) for g in gs]

    def calculate_pos(self, offset_x=0, offset_y=0, height=0, width=0):
        """Calculates absolute positions for nodes and nested groups, and returns groups's width and height"""

        # helpers
        def _adjust_for_border(x, y):
            return x + self.border, y + self.border

        def _stretch_group(x, y, width, height):
            return max(x, width), max(y, height)

        # adjust offset for border
        offset_x, offset_y = _adjust_for_border(offset_x, offset_y)

        # inherit absolute coordinates
        abs_x, abs_y = offset_x, offset_y

        # add initial gap if there are nodes
        if self.nodes:
            abs_x += 0 if self.horizontal else 0.5
            abs_y += 0.5 if self.horizontal else 0

        # place nodes
        for n in self.nodes:
            # add a gap before the node
            abs_x += 0.5 if self.horizontal else 0
            abs_y += 0 if self.horizontal else 0.5

            # set node coordinates
            n.x, n.y = abs_x, abs_y

            # add a gap after the node, considering the node's dimensions
            abs_x += 0.5 + n.width if self.horizontal else 0
            abs_y += 0 if self.horizontal else 0.5 + n.height

            # expand the container, considering nodes' dimensions in this group
            width, height = _stretch_group(abs_x + (not self.horizontal) * n.width, abs_y + self.horizontal * n.height, width, height)

        for g in self.groups:
            # calculate positions for the nested group and get the group's dimensions
            g_width, g_height = g.calculate_pos(abs_x, abs_y)

            # find the larger dimensions between the nested and the parent group
            width, height = _stretch_group(g_width, g_height, width, height)

            # add a gap after the group, considering the group's dimensions and whether the previous sibling had a border
            abs_x = width - g.border if self.horizontal else abs_x
            abs_y = abs_y if self.horizontal else height - g.border

            # expand the container
            width, height = _stretch_group(abs_x, abs_y, width, height)

        # add final gap if there are nodes
        if self.nodes:
            width += 0 if self.horizontal else 0.5
            height += 0.5 if self.horizontal else 0

        # save the group's position and dimensions
        self.x, self.y = offset_x, offset_y
        self.width, self.height = width - self.x, height - self.y

        # adjust offset for border
        width, height = _adjust_for_border(width, height)

        # return group's dimensions
        return width, height

__init__(design_rules=[])

Group's constructor

Source code in model/atoms/__init__.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def __init__(self, design_rules=[]):
    """Group's constructor"""
    super().__init__()

    self.design_rules = design_rules

    # absolute coordinates - calculated with calculate_pos()
    self.x = None
    self.y = None
    self.width = None
    self.height = None

    # nodes, edges, and nested groups
    self.nodes = AtomList()
    self.edges = AtomList()
    self.groups = AtomList()

__str__()

Returns string

Source code in model/atoms/__init__.py
97
98
99
def __str__(self):
    """Returns string"""
    return f"{self.__class__.__name__} with {len(self.nodes)} nodes, {len(self.edges)} edges, and {len(self.groups)} nested groups."

add_edge(e)

Adds an edge to this group

Source code in model/atoms/__init__.py
125
126
127
128
129
130
131
@enforce_allowed_types
@enforce_design_rules
def add_edge(self, e: Edge):
    """Adds an edge to this group"""
    if not any(isinstance(e, cls) for cls in self.allowed_edges):
        raise TypeError(f"{e.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[e.__name__ for e in self.allowed_edges]}")
    self.edges.append(e)

add_edges(es)

Adds list of edges to this group

Source code in model/atoms/__init__.py
145
146
147
def add_edges(self, es: AtomList):
    """Adds list of edges to this group"""
    [self.add_edge(e) for e in es]

add_group(g)

Adds a nested group to this group

Source code in model/atoms/__init__.py
133
134
135
136
137
138
139
@enforce_allowed_types
@enforce_design_rules
def add_group(self, g: Group):
    """Adds a nested group to this group"""
    if not any(isinstance(g, cls) for cls in self.allowed_groups):
        raise TypeError(f"{g.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[g.__name__ for g in self.allowed_groups]}")
    self.groups.append(g)

add_groups(gs)

Adds list of nested groups to this group

Source code in model/atoms/__init__.py
149
150
151
def add_groups(self, gs: AtomList):
    """Adds list of nested groups to this group"""
    [self.add_group(g) for g in gs]

add_node(n)

Adds a node to this group

Source code in model/atoms/__init__.py
117
118
119
120
121
122
123
@enforce_allowed_types
@enforce_design_rules
def add_node(self, n: Node):
    """Adds a node to this group"""
    if not any(isinstance(n, cls) for cls in self.allowed_nodes):
        raise TypeError(f"{n.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[n.__name__ for n in self.allowed_nodes]}")
    self.nodes.append(n)

add_nodes(ns)

Adds list of nodes to this group

Source code in model/atoms/__init__.py
141
142
143
def add_nodes(self, ns: AtomList):
    """Adds list of nodes to this group"""
    [self.add_node(n) for n in ns]

calculate_pos(offset_x=0, offset_y=0, height=0, width=0)

Calculates absolute positions for nodes and nested groups, and returns groups's width and height

Source code in model/atoms/__init__.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def calculate_pos(self, offset_x=0, offset_y=0, height=0, width=0):
    """Calculates absolute positions for nodes and nested groups, and returns groups's width and height"""

    # helpers
    def _adjust_for_border(x, y):
        return x + self.border, y + self.border

    def _stretch_group(x, y, width, height):
        return max(x, width), max(y, height)

    # adjust offset for border
    offset_x, offset_y = _adjust_for_border(offset_x, offset_y)

    # inherit absolute coordinates
    abs_x, abs_y = offset_x, offset_y

    # add initial gap if there are nodes
    if self.nodes:
        abs_x += 0 if self.horizontal else 0.5
        abs_y += 0.5 if self.horizontal else 0

    # place nodes
    for n in self.nodes:
        # add a gap before the node
        abs_x += 0.5 if self.horizontal else 0
        abs_y += 0 if self.horizontal else 0.5

        # set node coordinates
        n.x, n.y = abs_x, abs_y

        # add a gap after the node, considering the node's dimensions
        abs_x += 0.5 + n.width if self.horizontal else 0
        abs_y += 0 if self.horizontal else 0.5 + n.height

        # expand the container, considering nodes' dimensions in this group
        width, height = _stretch_group(abs_x + (not self.horizontal) * n.width, abs_y + self.horizontal * n.height, width, height)

    for g in self.groups:
        # calculate positions for the nested group and get the group's dimensions
        g_width, g_height = g.calculate_pos(abs_x, abs_y)

        # find the larger dimensions between the nested and the parent group
        width, height = _stretch_group(g_width, g_height, width, height)

        # add a gap after the group, considering the group's dimensions and whether the previous sibling had a border
        abs_x = width - g.border if self.horizontal else abs_x
        abs_y = abs_y if self.horizontal else height - g.border

        # expand the container
        width, height = _stretch_group(abs_x, abs_y, width, height)

    # add final gap if there are nodes
    if self.nodes:
        width += 0 if self.horizontal else 0.5
        height += 0.5 if self.horizontal else 0

    # save the group's position and dimensions
    self.x, self.y = offset_x, offset_y
    self.width, self.height = width - self.x, height - self.y

    # adjust offset for border
    width, height = _adjust_for_border(width, height)

    # return group's dimensions
    return width, height

enforce_allowed_types(func)

Decorator to enforce allowed types for primitives.

Source code in model/atoms/__init__.py
102
103
104
105
106
107
def enforce_allowed_types(func):
    """Decorator to enforce allowed types for primitives."""
    def wrapper(self, *args, **kwargs):
        print("Enforcing allowed types") # HERE COMES THE CHECK
        return func(self, *args, **kwargs)
    return wrapper

enforce_design_rules(func)

Decorator to enforce design rules after a primitive execution.

Source code in model/atoms/__init__.py
109
110
111
112
113
114
115
def enforce_design_rules(func):
    """Decorator to enforce design rules after a primitive execution."""
    def wrapper(self, *args, **kwargs):
        result = func(self, *args, **kwargs)
        [rule() for rule in self.design_rules]
        return result
    return wrapper

remove_edge(e)

Removes an edge from this group

Source code in model/atoms/__init__.py
160
161
162
163
164
def remove_edge(self, e: Edge):
    """Removes an edge from this group"""
    if type(e) not in self.allowed_edges:
        raise TypeError(f"{e.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[e.__name__ for e in self.allowed_edges]}")
    self.edges.remove(e)

remove_edges(es)

Removes a list of edges from this group

Source code in model/atoms/__init__.py
176
177
178
def remove_edges(self, es: list[Edge]):
    """Removes a list of edges from this group"""
    [self.remove_edge(e) for e in es]

remove_group(g)

Removes a nested group from this group

Source code in model/atoms/__init__.py
166
167
168
169
170
def remove_group(self, g: Group):
    """Removes a nested group from this group"""
    if type(g) not in self.allowed_groups:
        raise TypeError(f"{g.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[g.__name__ for g in self.allowed_groups]}")
    self.groups.remove(g)

remove_groups(gs)

Removes a list of groups from this group

Source code in model/atoms/__init__.py
180
181
182
def remove_groups(self, gs: list[Group]):
    """Removes a list of groups from this group"""
    [self.remove_group(g) for g in gs]

remove_node(n)

Removes a node from this group

Source code in model/atoms/__init__.py
153
154
155
156
157
158
def remove_node(self, n: Node):
    """Removes a node from this group"""
    if type(n) not in self.allowed_nodes:
        raise TypeError(f"{n.__class__.__name__} not allowed in {self.__class__.__name__}. Allowed: {[n.__name__ for n in self.allowed_nodes]}")
    self.nodes.remove(n)
    [self.remove_edge(e) for e in self.edges if e.u == n or e.v == n] # removing node also removes all associated edges

remove_nodes(ns)

Removes a list of nodes from this group

Source code in model/atoms/__init__.py
172
173
174
def remove_nodes(self, ns: list[Node]):
    """Removes a list of nodes from this group"""
    [self.remove_node(n) for n in ns]

Node

Bases: Atom

Node as in a graph

Source code in model/atoms/__init__.py
31
32
33
34
35
36
37
38
39
40
41
42
43
class Node(Atom):
    """Node as in a graph"""
    def __init__(self, x=0, y=0):
        """Node's constructor"""
        super().__init__()

        # absolute coordinates - calculated with calculate_pos()
        self.x = None
        self.y = None

        # node size
        self.width = 1
        self.height = 1

__init__(x=0, y=0)

Node's constructor

Source code in model/atoms/__init__.py
33
34
35
36
37
38
39
40
41
42
43
def __init__(self, x=0, y=0):
    """Node's constructor"""
    super().__init__()

    # absolute coordinates - calculated with calculate_pos()
    self.x = None
    self.y = None

    # node size
    self.width = 1
    self.height = 1