Skip to content

dome

DomeGenerator

Bases: BaseGenerator

Randomly samples and form-finds a radial dome from CEM trails converging on a center or central opening.

Each of n_trails radial CEM trails runs from the center (or, if opening, from a small central ring) outward to a supported outer ring, with per-ring deviation edges tying the trails together, and is equilibrated with MP-CEM.

Source code in src/torch_structure/generators/dome.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 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
class DomeGenerator(BaseGenerator):
    """Randomly samples and form-finds a radial dome from CEM trails converging on a center or central opening.

    Each of ``n_trails`` radial CEM trails runs from the center (or, if
    ``opening``, from a small central ring) outward to a supported outer
    ring, with per-ring deviation edges tying the trails together, and is
    equilibrated with MP-CEM.
    """

    def __init__(self, **overrides):
        super().__init__(**overrides)
        self.max_attempts = 100

        self.node_attrs = {
            "coords": torch.empty((0, 3), dtype=torch.float),
            "load": torch.empty((0, 3), dtype=torch.float),
            "support_condition": torch.empty((0, 3), dtype=torch.long),
            "is_origin_node": torch.empty((0, 1), dtype=torch.bool),
            "sequence": torch.empty((0, 1), dtype=torch.long),
        }
        self.edge_attrs = {
            "force": torch.empty((0, 1), dtype=torch.float),
            "length": torch.empty((0, 1), dtype=torch.float),
            "is_trail_edge": torch.empty((0, 1), dtype=torch.bool),
            "force_sign": torch.empty((0, 1), dtype=torch.float),
        }
        self.default_attrs = {
            "force": torch.tensor([torch.nan]),
            "length": torch.tensor([torch.nan]),
            "coords": torch.full((3,), torch.nan),
            "load": torch.zeros(3, dtype=torch.float),
            "support_condition": torch.zeros(3, dtype=torch.bool),
            "is_origin_node": torch.tensor(0, dtype=torch.bool),
        }

    def validate_input(
        self, n_trails, n_rings, trail_length, center_deviation_force, opening
    ):
        """Validate that ``n_trails`` is even; raises ``ValueError`` otherwise."""
        if n_trails % 2 != 0:
            raise ValueError("Number of trails must be even.")

    def sample_input(
        self,
        n_trails=None,
        n_rings=None,
        trail_length=None,
        center_deviation_force=None,
        opening=False,
    ):
        """Sample any unspecified generation parameters.

        Each parameter defaults to ``None``, meaning "sample randomly"; an
        explicit value passed via ``overrides`` (see
        [BaseGenerator][torch_structure.generators.base_generator.BaseGenerator])
        is left unchanged. Returns a dict of all resolved parameters, to be
        passed to [generate][torch_structure.generators.dome.DomeGenerator.generate].
        """
        if n_trails is None:
            n_trails = 2 * np.random.randint(5, 15)
        if n_rings is None:
            n_rings = np.random.randint(3, 6)
        if trail_length is None:
            trail_length = np.random.uniform(0.05, 0.2)
        if center_deviation_force is None:
            center_deviation_force = np.random.uniform(-1.0, -8.0)

        return {
            "n_trails": n_trails,
            "n_rings": n_rings,
            "trail_length": trail_length,
            "center_deviation_force": center_deviation_force,
            "opening": opening,
        }

    def generate(
        self, n_trails, n_rings, trail_length, center_deviation_force, opening
    ):
        """Build the trails and ring deviations, form-find with MP-CEM, and normalize the resulting geometry.

        If ``opening`` is ``False``, the trail origin nodes are merged into
        a single centroid node after form-finding ([fix_graph][torch_structure.generators.dome.DomeGenerator.fix_graph]).

        Returns:
            StructData: the form-found dome, scaled to unit horizontal
            radius and translated to non-negative z.

        Raises:
            RuntimeError: if any trail's distance to the centroid decreases
                outward ([filter][torch_structure.generators.dome.DomeGenerator.filter]), or if the resulting height is out
                of bounds.
        """
        # Initialize data object
        data = StructData(
            node_attrs=self.node_attrs,
            edge_attrs=self.edge_attrs,
            default_attrs=self.default_attrs,
        )

        # Create topology diagram
        centroid = torch.tensor([0.0, 0.0, 0.0])
        angles = torch.linspace(0, 2 * math.pi, n_trails + 1)[:-1]

        # Generate trails
        for i in range(n_trails):
            angle = angles[i]
            if opening:
                origin_diameter = trail_length
                x = torch.cos(angle) * 0.5 * origin_diameter
                y = torch.sin(angle) * 0.5 * origin_diameter
                origin_coords = centroid + torch.tensor([x, y, 0.0])
                origin_load = torch.tensor([0.0, 0.0, -1.0])
            else:
                origin_coords = centroid
                x_load = torch.cos(angle) * center_deviation_force
                y_load = torch.sin(angle) * center_deviation_force
                origin_load = torch.tensor([x_load, y_load, -1.0 / n_trails])
            self.generate_trail(
                data=data,
                origin_coords=origin_coords,
                origin_load=origin_load,
                id=i,
                n_rings=n_rings,
                trail_length=trail_length,
            )

        # Add ring deviations
        for i in range(1, n_rings):
            force_sign = torch.randint(2, (1,)) * 2 - 1
            force_magnitude = torch.rand(1) * 2 + 1
            force = force_sign * force_magnitude
            for j in range(n_trails):
                data.add_edge(
                    f"trail_{j}_node_{i}",
                    f"trail_{(j + 1) % n_trails}_node_{i}",
                    is_trail_edge=torch.tensor(False),
                    force=force,
                )

        if opening:
            ring_force = center_deviation_force * origin_diameter * 50
            for i in range(n_trails):
                data.add_edge(
                    f"trail_{i}_node_0",
                    f"trail_{(i + 1) % n_trails}_node_0",
                    is_trail_edge=torch.tensor(False),
                    force=ring_force,
                )

        # Formfinding
        data = data.mpcem()
        if not opening:
            self.fix_graph(data, n_trails)

        if self.filter(data):
            raise RuntimeError("Negative inclination detected.")

        # Scale to unit length
        radius = torch.norm(data.coords[:, 0:2], dim=1).max()
        data.coords /= radius

        # Translate to positive coordinates
        data.coords[:, 2] -= data.coords[:, 2].min()

        # Set support
        data.is_support = data.is_support

        if data.bbox[0, 2] < 0 or data.bbox[1, 2] > 3.0:
            raise RuntimeError("Z out of bounds:", data.bbox[0, 2], data.bbox[1, 2])

        return data

    def generate_trail(
        self, data, origin_coords, origin_load, id, n_rings, trail_length
    ):
        """Add a single radial CEM trail (``n_rings`` edges) from an origin node to a supported outer node, in place."""
        data.add_node(
            f"trail_{id}_node_0",
            coords=origin_coords,
            is_origin_node=torch.tensor(True),
            sequence=torch.tensor(0),
            load=origin_load,
        )
        for i in range(1, n_rings + 1):
            support_condition = (
                torch.tensor([True, True, True])
                if i == n_rings
                else torch.tensor([False, False, False])
            )
            data.add_node(
                f"trail_{id}_node_{i}",
                is_origin_node=torch.tensor(False),
                sequence=torch.tensor(i),
                load=torch.tensor([0.0, 0.0, -1.0]),
                support_condition=support_condition,
            )
            data.add_edge(
                f"trail_{id}_node_{i - 1}",
                f"trail_{id}_node_{i}",
                is_trail_edge=torch.tensor(True),
                length=trail_length,
                force_sign=torch.tensor(-1.0),
            )

    def fix_graph(self, data, n_trails):
        """Merge all trails' origin nodes into a single centroid node, in place."""
        data.add_node(
            "centroid",
            coords=torch.tensor([0.0, 0.0, 0.0]),
            load=torch.tensor([0.0, 0.0, -1.0]),
        )
        merge_nodes = [f"trail_{i}_node_0" for i in range(n_trails)]
        data.merge_nodes("centroid", merge_nodes)

    def filter(self, data):
        """Check whether any trail edge's outer node is closer to the centroid than its inner node.

        Returns:
            bool: ``True`` if such an invalid (non-monotonically-outward)
            trail edge exists.
        """
        mask = (data.is_trail_edge & data.directed_mask).view(-1)
        src, dst = data.edge_index[:, mask]
        src_centroid_distance = torch.norm(data.coords[src, 0:2], dim=1)
        dst_centroid_distance = torch.norm(data.coords[dst, 0:2], dim=1)
        invalid = dst_centroid_distance < src_centroid_distance
        return invalid.any()

filter(data)

Check whether any trail edge's outer node is closer to the centroid than its inner node.

Returns:

Name Type Description
bool

True if such an invalid (non-monotonically-outward)

trail edge exists.

Source code in src/torch_structure/generators/dome.py
223
224
225
226
227
228
229
230
231
232
233
234
235
def filter(self, data):
    """Check whether any trail edge's outer node is closer to the centroid than its inner node.

    Returns:
        bool: ``True`` if such an invalid (non-monotonically-outward)
        trail edge exists.
    """
    mask = (data.is_trail_edge & data.directed_mask).view(-1)
    src, dst = data.edge_index[:, mask]
    src_centroid_distance = torch.norm(data.coords[src, 0:2], dim=1)
    dst_centroid_distance = torch.norm(data.coords[dst, 0:2], dim=1)
    invalid = dst_centroid_distance < src_centroid_distance
    return invalid.any()

fix_graph(data, n_trails)

Merge all trails' origin nodes into a single centroid node, in place.

Source code in src/torch_structure/generators/dome.py
213
214
215
216
217
218
219
220
221
def fix_graph(self, data, n_trails):
    """Merge all trails' origin nodes into a single centroid node, in place."""
    data.add_node(
        "centroid",
        coords=torch.tensor([0.0, 0.0, 0.0]),
        load=torch.tensor([0.0, 0.0, -1.0]),
    )
    merge_nodes = [f"trail_{i}_node_0" for i in range(n_trails)]
    data.merge_nodes("centroid", merge_nodes)

generate(n_trails, n_rings, trail_length, center_deviation_force, opening)

Build the trails and ring deviations, form-find with MP-CEM, and normalize the resulting geometry.

If opening is False, the trail origin nodes are merged into a single centroid node after form-finding (fix_graph).

Returns:

Name Type Description
StructData

the form-found dome, scaled to unit horizontal

radius and translated to non-negative z.

Raises:

Type Description
RuntimeError

if any trail's distance to the centroid decreases outward (filter), or if the resulting height is out of bounds.

Source code in src/torch_structure/generators/dome.py
 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
def generate(
    self, n_trails, n_rings, trail_length, center_deviation_force, opening
):
    """Build the trails and ring deviations, form-find with MP-CEM, and normalize the resulting geometry.

    If ``opening`` is ``False``, the trail origin nodes are merged into
    a single centroid node after form-finding ([fix_graph][torch_structure.generators.dome.DomeGenerator.fix_graph]).

    Returns:
        StructData: the form-found dome, scaled to unit horizontal
        radius and translated to non-negative z.

    Raises:
        RuntimeError: if any trail's distance to the centroid decreases
            outward ([filter][torch_structure.generators.dome.DomeGenerator.filter]), or if the resulting height is out
            of bounds.
    """
    # Initialize data object
    data = StructData(
        node_attrs=self.node_attrs,
        edge_attrs=self.edge_attrs,
        default_attrs=self.default_attrs,
    )

    # Create topology diagram
    centroid = torch.tensor([0.0, 0.0, 0.0])
    angles = torch.linspace(0, 2 * math.pi, n_trails + 1)[:-1]

    # Generate trails
    for i in range(n_trails):
        angle = angles[i]
        if opening:
            origin_diameter = trail_length
            x = torch.cos(angle) * 0.5 * origin_diameter
            y = torch.sin(angle) * 0.5 * origin_diameter
            origin_coords = centroid + torch.tensor([x, y, 0.0])
            origin_load = torch.tensor([0.0, 0.0, -1.0])
        else:
            origin_coords = centroid
            x_load = torch.cos(angle) * center_deviation_force
            y_load = torch.sin(angle) * center_deviation_force
            origin_load = torch.tensor([x_load, y_load, -1.0 / n_trails])
        self.generate_trail(
            data=data,
            origin_coords=origin_coords,
            origin_load=origin_load,
            id=i,
            n_rings=n_rings,
            trail_length=trail_length,
        )

    # Add ring deviations
    for i in range(1, n_rings):
        force_sign = torch.randint(2, (1,)) * 2 - 1
        force_magnitude = torch.rand(1) * 2 + 1
        force = force_sign * force_magnitude
        for j in range(n_trails):
            data.add_edge(
                f"trail_{j}_node_{i}",
                f"trail_{(j + 1) % n_trails}_node_{i}",
                is_trail_edge=torch.tensor(False),
                force=force,
            )

    if opening:
        ring_force = center_deviation_force * origin_diameter * 50
        for i in range(n_trails):
            data.add_edge(
                f"trail_{i}_node_0",
                f"trail_{(i + 1) % n_trails}_node_0",
                is_trail_edge=torch.tensor(False),
                force=ring_force,
            )

    # Formfinding
    data = data.mpcem()
    if not opening:
        self.fix_graph(data, n_trails)

    if self.filter(data):
        raise RuntimeError("Negative inclination detected.")

    # Scale to unit length
    radius = torch.norm(data.coords[:, 0:2], dim=1).max()
    data.coords /= radius

    # Translate to positive coordinates
    data.coords[:, 2] -= data.coords[:, 2].min()

    # Set support
    data.is_support = data.is_support

    if data.bbox[0, 2] < 0 or data.bbox[1, 2] > 3.0:
        raise RuntimeError("Z out of bounds:", data.bbox[0, 2], data.bbox[1, 2])

    return data

generate_trail(data, origin_coords, origin_load, id, n_rings, trail_length)

Add a single radial CEM trail (n_rings edges) from an origin node to a supported outer node, in place.

Source code in src/torch_structure/generators/dome.py
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
def generate_trail(
    self, data, origin_coords, origin_load, id, n_rings, trail_length
):
    """Add a single radial CEM trail (``n_rings`` edges) from an origin node to a supported outer node, in place."""
    data.add_node(
        f"trail_{id}_node_0",
        coords=origin_coords,
        is_origin_node=torch.tensor(True),
        sequence=torch.tensor(0),
        load=origin_load,
    )
    for i in range(1, n_rings + 1):
        support_condition = (
            torch.tensor([True, True, True])
            if i == n_rings
            else torch.tensor([False, False, False])
        )
        data.add_node(
            f"trail_{id}_node_{i}",
            is_origin_node=torch.tensor(False),
            sequence=torch.tensor(i),
            load=torch.tensor([0.0, 0.0, -1.0]),
            support_condition=support_condition,
        )
        data.add_edge(
            f"trail_{id}_node_{i - 1}",
            f"trail_{id}_node_{i}",
            is_trail_edge=torch.tensor(True),
            length=trail_length,
            force_sign=torch.tensor(-1.0),
        )

sample_input(n_trails=None, n_rings=None, trail_length=None, center_deviation_force=None, opening=False)

Sample any unspecified generation parameters.

Each parameter defaults to None, meaning "sample randomly"; an explicit value passed via overrides (see BaseGenerator) is left unchanged. Returns a dict of all resolved parameters, to be passed to generate.

Source code in src/torch_structure/generators/dome.py
51
52
53
54
55
56
57
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
def sample_input(
    self,
    n_trails=None,
    n_rings=None,
    trail_length=None,
    center_deviation_force=None,
    opening=False,
):
    """Sample any unspecified generation parameters.

    Each parameter defaults to ``None``, meaning "sample randomly"; an
    explicit value passed via ``overrides`` (see
    [BaseGenerator][torch_structure.generators.base_generator.BaseGenerator])
    is left unchanged. Returns a dict of all resolved parameters, to be
    passed to [generate][torch_structure.generators.dome.DomeGenerator.generate].
    """
    if n_trails is None:
        n_trails = 2 * np.random.randint(5, 15)
    if n_rings is None:
        n_rings = np.random.randint(3, 6)
    if trail_length is None:
        trail_length = np.random.uniform(0.05, 0.2)
    if center_deviation_force is None:
        center_deviation_force = np.random.uniform(-1.0, -8.0)

    return {
        "n_trails": n_trails,
        "n_rings": n_rings,
        "trail_length": trail_length,
        "center_deviation_force": center_deviation_force,
        "opening": opening,
    }

validate_input(n_trails, n_rings, trail_length, center_deviation_force, opening)

Validate that n_trails is even; raises ValueError otherwise.

Source code in src/torch_structure/generators/dome.py
44
45
46
47
48
49
def validate_input(
    self, n_trails, n_rings, trail_length, center_deviation_force, opening
):
    """Validate that ``n_trails`` is even; raises ``ValueError`` otherwise."""
    if n_trails % 2 != 0:
        raise ValueError("Number of trails must be even.")