Skip to content

nervi_dome

NerviDome

Builds a Nervi-style ribbed dome: radial CEM trails linked by diagonal (rather than ring) deviation edges.

Unlike DomeGenerator, this class takes fully specified (not randomly sampled) parameters, links consecutive rings diagonally rather than circumferentially, and only builds the graph in __init__ without form-finding it.

Source code in src/torch_structure/generators/nervi_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
class NerviDome:
    """Builds a Nervi-style ribbed dome: radial CEM trails linked by diagonal (rather than ring) deviation edges.

    Unlike [DomeGenerator][torch_structure.generators.dome.DomeGenerator], this
    class takes fully specified (not randomly sampled) parameters, links
    consecutive rings diagonally rather than circumferentially, and only
    builds the graph in ``__init__`` without form-finding it.
    """

    def __init__(
        self,
        n_trails,
        n_rings,
        trail_length,
        deviation_force,
        center_deviation_force,
        ring_force,
        opening_diameter,
        opening=True,
    ):
        # Topological parameters
        self.n_trails = n_trails
        self.n_rings = n_rings
        self.opening = opening

        # Metric parameters
        self.trail_length = trail_length
        self.center_deviation_force = center_deviation_force
        self.ring_force = ring_force
        self.opening_diameter = opening_diameter
        self.deviation_force = deviation_force

        # if self.n_trails % 2 != 0:
        #     raise ValueError('Number of trails must be even.')  # doublecheck

        self.generate_graph()
        if not self.opening:
            self.fix_graph()

    def generate_graph(self):
        """Build the radial trails and diagonal ring deviations into ``self.graph`` (not yet form-found)."""
        # Initialize data object
        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),
        }
        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),
            "source_sequence": torch.empty((0, 1), dtype=torch.long),
        }
        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),
        }
        self.graph = StructData(
            node_attrs=node_attrs, edge_attrs=edge_attrs, default_attrs=default_attrs
        )

        # Create topology diagram
        centroid = torch.tensor([0.0, 0.0, 0.0])
        angles = torch.linspace(0, 2 * math.pi, self.n_trails + 1)[:-1]
        # pi = math.pi
        # print(f'{pi:.200f}')
        # angles = torch.tensor([0, 0.5*pi, pi, 1.5*pi])

        # manual_radius = 0.75
        # manual_origin_coords = [torch.tensor([manual_radius, 0.0000, 0.0000]),
        #                         torch.tensor([0.0000,  manual_radius,  0.0000]),
        #                         torch.tensor([-manual_radius, 0.0000,  0.0000]),
        #                         torch.tensor([0.0000, -manual_radius,  0.0000])]

        # Generate trails
        for i in range(self.n_trails):
            angle = angles[i]
            if self.opening:
                origin_diameter = self.opening_diameter
                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_coords = manual_origin_coords[i]
                origin_load = torch.tensor([0.0, 0.0, -1.0])
            else:
                origin_coords = centroid
                x_load = torch.cos(angle) * self.center_deviation_force
                y_load = torch.sin(angle) * self.center_deviation_force
                origin_load = torch.tensor([x_load, y_load, -1.0 / self.n_trails])
            self.generate_trail(
                origin_coords=origin_coords, origin_load=origin_load, id=i
            )

        # Add ring deviations
        for i in range(0, self.n_rings):
            for j in range(self.n_trails):
                self.graph.add_edge(
                    f"trail_{j}_node_{i}",
                    f"trail_{(j + 1) % self.n_trails}_node_{i + 1}",
                    is_trail_edge=torch.tensor(False),
                    force=self.deviation_force,
                    source_sequence=torch.tensor(i),
                )

        if self.opening:
            for i in range(self.n_trails):
                self.graph.add_edge(
                    f"trail_{i}_node_0",
                    f"trail_{(i + 1) % self.n_trails}_node_0",
                    is_trail_edge=torch.tensor(False),
                    force=self.ring_force,
                    source_sequence=torch.tensor(-1),
                )

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

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

fix_graph()

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

Source code in src/torch_structure/generators/nervi_dome.py
166
167
168
169
170
171
172
173
174
def fix_graph(self):
    """Merge all trails' origin nodes into a single centroid node, in place."""
    self.graph.add_node(
        "centroid",
        coords=torch.tensor([0.0, 0.0, 0.0]),
        load=torch.tensor([0.0, 0.0, 0.0]),
    )
    merge_nodes = [f"trail_{i}_node_0" for i in range(self.n_trails)]
    self.graph.merge_nodes("centroid", merge_nodes)

generate_graph()

Build the radial trails and diagonal ring deviations into self.graph (not yet form-found).

Source code in src/torch_structure/generators/nervi_dome.py
 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
def generate_graph(self):
    """Build the radial trails and diagonal ring deviations into ``self.graph`` (not yet form-found)."""
    # Initialize data object
    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),
    }
    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),
        "source_sequence": torch.empty((0, 1), dtype=torch.long),
    }
    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),
    }
    self.graph = StructData(
        node_attrs=node_attrs, edge_attrs=edge_attrs, default_attrs=default_attrs
    )

    # Create topology diagram
    centroid = torch.tensor([0.0, 0.0, 0.0])
    angles = torch.linspace(0, 2 * math.pi, self.n_trails + 1)[:-1]
    # pi = math.pi
    # print(f'{pi:.200f}')
    # angles = torch.tensor([0, 0.5*pi, pi, 1.5*pi])

    # manual_radius = 0.75
    # manual_origin_coords = [torch.tensor([manual_radius, 0.0000, 0.0000]),
    #                         torch.tensor([0.0000,  manual_radius,  0.0000]),
    #                         torch.tensor([-manual_radius, 0.0000,  0.0000]),
    #                         torch.tensor([0.0000, -manual_radius,  0.0000])]

    # Generate trails
    for i in range(self.n_trails):
        angle = angles[i]
        if self.opening:
            origin_diameter = self.opening_diameter
            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_coords = manual_origin_coords[i]
            origin_load = torch.tensor([0.0, 0.0, -1.0])
        else:
            origin_coords = centroid
            x_load = torch.cos(angle) * self.center_deviation_force
            y_load = torch.sin(angle) * self.center_deviation_force
            origin_load = torch.tensor([x_load, y_load, -1.0 / self.n_trails])
        self.generate_trail(
            origin_coords=origin_coords, origin_load=origin_load, id=i
        )

    # Add ring deviations
    for i in range(0, self.n_rings):
        for j in range(self.n_trails):
            self.graph.add_edge(
                f"trail_{j}_node_{i}",
                f"trail_{(j + 1) % self.n_trails}_node_{i + 1}",
                is_trail_edge=torch.tensor(False),
                force=self.deviation_force,
                source_sequence=torch.tensor(i),
            )

    if self.opening:
        for i in range(self.n_trails):
            self.graph.add_edge(
                f"trail_{i}_node_0",
                f"trail_{(i + 1) % self.n_trails}_node_0",
                is_trail_edge=torch.tensor(False),
                force=self.ring_force,
                source_sequence=torch.tensor(-1),
            )

generate_trail(origin_coords, origin_load, id)

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