Seongsu-CELL
Botanical Cultural Complex Proposal, Seongsu-dong
| period | 2024.03–2024.06 · follow-up development 2026.02 |
| location | 89 Yeonmujang-gil, Seongdong-gu, Seoul, South Korea |
| project | Individual work · fourth-year architectural design studio |
| status | Unbuilt academic proposal · follow-up design development |
| goal | Spatial organization of a botanical cultural complex using 3D Voronoi cells |
| result | Seed-point arrangement rules, optimization model, and 3D Power Diagram component |
Seongsu-CELL is an individual project exploring how 3D Voronoi cells can organize a botanical cultural complex. In February 2026, the project added a custom Power Diagram to reconsider the spatial and structural hierarchy of those cells. The 2024 proposal developed its scheme through seed-point arrangement rules and an annual solar-radiation search. It left unresolved how to adjust cell sizes and floor plates for different programs, and how to translate complex boundaries into structural and construction logic. The follow-up work revisited these problems by controlling cell sizes through weights.


Cellular Tissue and Three Programmatic Layers
The repeated divisions observed in a garlic cross-section provided the initial visual reference. The spatial organization drew from the interaction between cell arrangements, the roles of different tissues, and nutrient-transport pathways within a dicot stem.
The project translated this relationship into three programmatic layers. Core cells accommodate vertical circulation and services. Garden cells form the main interior spaces. Perimeter cells correspond to the epidermal tissue and accommodate visitor circulation and event spaces. The design translated the division of roles between tissues into a spatial principle; it did not attempt a literal biological replica.


Seed-Point Arrangements for Architectural Space
Freely placed seed points generate formally varied cells, but they do not reliably secure minimum floor area, usable floor slopes, or floor-to-floor height. The study first compared how distances and arrangements between seed points affected these three conditions.




The left diagram shows the sectional rule, which aligns seed points vertically according to each floor level. Fixing their heights and vertical stacks established regular floors and cell heights. The right diagram shows the radial rule, which positions seed points within three concentric bands. The spacing between bands secured minimum floor area, while different movement ranges adjusted the relative sizes of the core, garden, and perimeter cells. The center model combines both rules.
When plan positions differed from floor to floor, the vertical stacks became misaligned and the floor plates tilted. It was possible to retain only plates within an allowable slope, but adjusting every combination manually was slow and lacked a consistent selection criterion. This condition was therefore included in the Galapagos search.
Spiral Organization and Performance Search



The left model shows the initial configuration based on the vertical and radial rules. In the center model, the garden and perimeter cells wrap around the core and rise in two-storey increments. This stage established the visitor route that connects the spaces through ramps and stairs. The right model shows the result of using Galapagos to fine-tune the seed points in plan while preserving these relationships.

Cell generation, floor-plate evaluation, and performance analysis were assembled within a single Grasshopper definition. The seed-point heights, three programmatic layers, and spiral arrangement remained fixed. Only the plan positions of the seed points varied within their radial bands.
For each option, the system checked floor area and normal vectors to identify slabs that met the minimum-area and allowable-slope criteria. It then combined this validity result with the annual solar radiation received by the valid floor plates into a single fitness value for Galapagos to maximize. The search sought seed-point arrangements that provided both usable floors and solar performance.
Two Peaks Across 132 Generations



Rapid convergence on a single peak was not the sole objective. To compare spatial possibilities emerging at different peaks, the input resolution was reduced to widen the search intervals. A logarithmic transformation and digit truncation were applied to the fitness value so that minor differences within the high-scoring range would not dominate the search.
The Galapagos Inbreeding setting was also adjusted so that mating would not concentrate only among genomes with similar gene values. This left room for different genetic combinations to participate and made it possible to observe local optima that might otherwise be obscured by a single family of solutions. The setting did not guarantee diversity, but it allowed forms outside the performance ranking to remain available as design alternatives.
The 132-generation record showed two distinct peaks. One local optimum first appeared in Generation 15, disappeared from the population, and reappeared in Generation 50. Another lineage improved after Generation 25 and held the highest fitness from Generation 73 onward.



The solutions converging on the highest peak distributed valid floors and solar performance evenly. The local optimum at the other peak scored lower but produced more pronounced mid-level projections and a clearer spatial hierarchy. The final proposal developed from the latter form.


The Gap Between Generated Cells and Architecture
The original proposal constrained Voronoi irregularity through arrangement rules and performance criteria, but it could not control the scale of each programmatic cell independently. Cell sizes were determined indirectly by the distances between seed points, while changes in their plan positions altered floor slopes.
Voronoi edges defined spatial boundaries but did not guarantee stable load paths or repeatable joints. The original proposal concluded as a studio project without resolving how to translate its branching frames into rational members and connections.
The 2026 follow-up began by treating cell size as an independent parameter and connecting the spatial and structural hierarchies.
A 3D Power Diagram for Weighted Cells
In February 2026, a 3D Power Diagram was implemented in Python for use in Grasshopper, allowing each seed point to receive a different size parameter.




For an ordinary Voronoi diagram, Delaunay Triangulation determines neighboring relationships from seed-point positions alone. A Power Diagram uses Regular Triangulation, a weighted Delaunay subdivision that accounts for both position and weight. The size parameter can therefore adjust the relative scale and adjacency of cells within the same seed-point arrangement.
The component squares the input size parameter R and uses it as the power weight. Each seed point is lifted to the four-dimensional coordinate (x, y, z, x²+y²+z²−R²), and the lower convex hull provides the tetrahedral connections of the Regular Triangulation. The power centers of these tetrahedra become the cell vertices, which are assembled and clipped by the specified boundary to produce 3D cell meshes.
The component retains the input-output workflow of the existing 3D Voronoi component and adds the size-parameter list. This allowed it to connect to the existing Grasshopper definition without rebuilding the workflow.
Spatial Hierarchy Through Weighting



The left model is the follow-up baseline, generated after rearranging the seed points but before applying differentiated weights. The center model applies weights according to program and floor level. The right model distinguishes the resulting spatial and structural hierarchies.
Cells within the same radial layer and floor shared a weight range, while the core, garden, and perimeter cells received different ranges. The seed-point heights remained fixed, and only these weights became Galapagos variables. The ranges preserved the three-layer programmatic hierarchy under every combination, and the result with the highest annual-solar-radiation fitness was used for the development model.
Adjusting cell sizes without moving the seed points in plan kept the floor plates generally horizontal. It also preserved the original spiral arrangement of the garden and perimeter spaces around the core. The core and perimeter cells form the structural framework and accommodate service and event spaces, while the larger cells between them contain the botanical garden. The three-layer relationship derived from the plant stem became explicit as a programmatic and spatial hierarchy.

Three Spatial Layers



Core cells accommodate the elevator halls and service circulation at every level. The garden cells form the double-height main spaces, with their floors lowered below the surrounding circulation to provide sufficient soil depth. Perimeter cells contain single-storey visitor and interactive circulation as well as event spaces.
Core cells occur at every level, while the garden and perimeter spaces connect in two-storey increments. A spiral segment beginning at Level 1 meets the next segment at Level 3, which then continues to Level 5. Ramps and stairs connect these segments. The intermediate core and perimeter cells act as mezzanines adjoining the double-height garden spaces, providing access and views from different elevations.


Structural Planning from Spatial Hierarchy



Weights controlled both cell size and directionality. Reducing the weights of the perimeter cells while increasing those of adjacent cells narrowed the perimeter cells horizontally and preserved their vertical extent. This produced elongated cells with a strong vertical anisotropy around the perimeter. The vertically elongated cells simplify load paths and translate the arrangement of the plant stem’s epidermal cells into structural form.
The structure was organized in three tiers. Primary vertical members follow the axes of the core and perimeter cells. Secondary vertical members support the double-height garden cells and supplement the intended load path for planting loads. Horizontal ties connect the vertical members to control lateral deformation and torsion, directing loads toward the primary members at the core and perimeter.
A separate Galapagos search simplified the perimeter frame. Nearby vertices and edges within a set tolerance were merged, while the heights and weights of the perimeter seed points became variables. The search maintained the allowable range of floor slopes while minimizing the remaining edge count, aiming to reduce the number of members and increase repetition.
Development Model and Remaining Work
The weighted cells address the original proposal’s difficulty in controlling spatial scale and horizontal floor plates. The current result organizes programmatic and structural hierarchies within the same model.
Structural analysis and member calculations have not yet been conducted. Member sizing and joint design, together with the integration of drainage and building services into the planted spaces, remain unresolved. Converting the Voronoi-generated form into buildable architecture remains the next task for Seongsu-CELL.
Power Diagram - Grasshopper python component Script
1#r: numpy
2#r: scipy
3import numpy as np
4from scipy.spatial import ConvexHull
5import Rhino.Geometry as rg
6import System
7import System.Threading.Tasks as tasks
8
9'''
10= 3D Weighted Voronoi Algorithm - Power Diagram =
11- Rhino8 Grasshopper, Python 3 Script Component -
12
13[Component Input]
14- P (List Access, Point3d): Voronoi Seed Points
15- R (List Access, float): Weights of Seed Points
16- B (Item Access, Brep): Voronoi Boundary
17
18[Component Output]
19- M (Mesh): Voronoi Cells
20- D (Curve): Regular Triangulation
21
22Last edit by. HelixHo 02/16/2026
23'''
24
25### Global Tolerance
26tol = 0.001
27
28### Parallel Operations ProcessorCount
29options = tasks.ParallelOptions()
30options.MaxDegreeOfParallelism = System.Environment.ProcessorCount - 2
31
32###### Calculate simplex 3D Volume (simplex validation)
33def get_simplex_volume_3d(pts_4d):
34 # pts_4d: [[x,y,z,w], [x,y,z,w], [x,y,z,w], [x,y,z,w]] (4x4 array)
35 pts_3d = pts_4d[:, :3]
36 v1 = pts_3d[1] - pts_3d[0]
37 v2 = pts_3d[2] - pts_3d[0]
38 v3 = pts_3d[3] - pts_3d[0]
39 matrix = np.array([v1, v2, v3])
40
41 return abs(np.linalg.det(matrix)) / 6.0
42
43###### Calculate Power Center (vertices of Weighted Voronoi)
44# ∣∣X-P_i∣∣^2 − w_i = ∣∣X-P_j∣∣^2 − w_j
45# (x-x_i)^2 + (y-y_i)^2 + (z-z_i)^2 - w_i = (x-x_j)^2 + (y-y_j)^2 + (z-z_j)^2 - w_j
46# 2x(x_j-x_i) + 2y(y_j-y_i) + 2z(z_j-z_i) = (x_j^2 + y_j^2 + z_j^2) - (x_i^2 + y_i^2 + z_i^2) - (w_j-w_i)
47# 2X⋅(P_i-P_j) = ∣∣P_i∣∣^2 − ∣∣P_j∣∣^2 − (w_i−w_j)
48def solve_power_center(pts, weights):
49 A, B = [], []
50 p0, w0 = pts[0], weights[0]
51
52 for i in range(1, 4):
53 pi, wi = pts[i], weights[i]
54 A.append([2 * (pi[j] - p0[j]) for j in range(3)])
55 B.append(np.sum(pi**2) - np.sum(p0**2) - (wi - w0))
56
57 try:
58 res = np.linalg.solve(np.array(A, dtype=float), np.array(B, dtype=float))
59 return rg.Point3d(float(res[0]), float(res[1]), float(res[2]))
60 except:
61 return None
62
63###### Parallel Operations Section: Individual Task Functions
64# Generate Voronoi Cell (polyhedron)
65def construct_single_cell(i, simplices, p_centers, B_mesh, move_back, tol):
66 try:
67 # i: Index of Seed Point (from. Input: P)
68 # Vertex extraction
69 sim_indices = [idx for idx, sim in enumerate(simplices) if i in sim]
70 if len(sim_indices) < 4: return None
71 v_pts = [p_centers[idx] for idx in sim_indices if p_centers[idx] is not None]
72 if len(v_pts) < 4: return None
73
74 # Vertex integration stabilization (rounding to 7th digit)
75 snapped = [rg.Point3d(round(v.X, 7), round(v.Y, 7), round(v.Z, 7)) for v in v_pts]
76 unique_v = list(dict.fromkeys(snapped))
77 if len(unique_v) < 4: return None
78
79 # Create Meshes
80 temp_mesh = rg.Mesh.CreateConvexHull3D(unique_v, tol, tol)
81 if not temp_mesh: return None
82 cell_mesh = temp_mesh[0]
83
84 # Trim with Voronoi border (from. Input: B)
85 intersections = rg.Mesh.CreateBooleanIntersection([cell_mesh], [B_mesh])
86
87 if intersections and len(intersections) > 0:
88 res_mesh = intersections[0]
89 res_mesh.Transform(move_back)
90 return res_mesh
91
92 except Exception as e:
93 return None
94 return None
95
96###### Generate Bounding Seed Points
97def get_26_anchors(bbox, scale=5.0):
98 big_box = bbox
99 big_box.Inflate(bbox.Diagonal.Length * scale)
100
101 corners = []
102 for x in [0, 0.5, 1]:
103 for y in [0, 0.5, 1]:
104 for z in [0, 0.5, 1]:
105 if x == 0.5 and y == 0.5 and z == 0.5: continue
106 corners.append(big_box.PointAt(x, y, z))
107 return corners
108
109############################################################
110### MAIN ### 3D Weighted Voronoi (POWER DIAGRAM) Algorithm
111############################################################
112def main():
113 if not P or not R or not B:
114 return [], None
115
116 ### --- 1. Data Set
117 W = [r**2 for r in R]
118
119 anchor_pts = get_26_anchors(rg.BoundingBox(P), 2.0)
120 total_pts = P + anchor_pts
121 total_ws = W + [0.0] * len(anchor_pts)
122
123 ## Move closer to World Origin
124 pts_center = rg.BoundingBox(total_pts).Center
125 move_back = rg.Transform.Translation(rg.Vector3d(pts_center))
126 P_local = [p - pts_center for p in total_pts]
127
128 move_matrix = rg.Transform.Translation(-rg.Vector3d(pts_center))
129 B_local = B.DuplicateBrep()
130 B_local.Transform(move_matrix)
131
132 ## Convert to numpy.array
133 np_pts = np.array([[p.X, p.Y, p.Z] for p in P_local], dtype=float)
134 np_ws = np.array(total_ws, dtype=float)
135
136 ## Normalize Input Data
137 max_coord = np.max(np.abs(np_pts))
138 if max_coord == 0:
139 max_coord = 1.0
140 np_pts_norm = np_pts / max_coord
141 np_ws_norm = np_ws / (max_coord**2)
142
143 ### --- 2. 4D Lifting & Convex Hull
144 # 4th Coord = x^2+y^2+z+2-w, w=r^2
145 lifted_coord = np.sum(np_pts_norm**2, axis=1) - np_ws_norm
146 lifted = np.column_stack((np_pts_norm, lifted_coord))
147
148 hull = ConvexHull(lifted, qhull_options='QJ')
149
150 ## Select Lower Hull & Simplex Validation
151 # hull.equations: numpy.list(A B C D E) of Ax+By+Cz+Dw+E = 0
152 lower_hull_indices = []
153 for i, eq in enumerate(hull.equations):
154 if eq[3] < -1e-7:
155 pts_4d = lifted[hull.simplices[i]]
156 vol = get_simplex_volume_3d(pts_4d)
157 if abs(vol) > 1e-7:
158 lower_hull_indices.append(i)
159
160 # hull.simplices: tetrahedron on 4D
161 # simplices: 3D Regular Triangulation (tetrahedron)
162 simplices = [hull.simplices[idx] for idx in lower_hull_indices]
163
164 ### --- 3. Get Power Center of each simplices
165 p_centers = []
166 for sim in simplices:
167 center_norm = solve_power_center(np_pts_norm[sim], np_ws_norm[sim])
168 # Return to original coord
169 if center_norm is not None:
170 p_centers.append(center_norm * max_coord)
171 else:
172 p_centers.append(None)
173
174 ### --- 4. Generate Voronoi Cell
175 ## Convert Voronoi border(Brep) to Mesh (from. Input: B)
176 mp = rg.MeshingParameters.Default
177 raw_meshes = rg.Mesh.CreateFromBrep(B_local, mp)
178 final_bd_mesh = rg.Mesh()
179 for m in raw_meshes:
180 final_bd_mesh.Append(m)
181 final_bd_mesh.Vertices.CombineIdentical(True, True)
182 final_bd_mesh.Compact()
183 final_bd_mesh.Normals.ComputeNormals()
184
185 ## Parallel Operations (Generate Voronoi Cell)
186 num_pts = len(P)
187 result_cells = [None] * num_pts
188
189 def parallel_body(i):
190 result_cells[i] = construct_single_cell(i, simplices, p_centers, final_bd_mesh, move_back, tol)
191
192 tasks.Parallel.For(0, num_pts, options, parallel_body)
193
194 ### --- 5. Extract 3D Regular Triangulation (WireFrame)
195 d_lines = []
196 edges = set()
197
198 num_input_pts = len(P)
199
200 ## Possible edge combinations from a tetrahedron (6 total)
201 # (0,1), (0,2), (0,3), (1,2), (1,3), (2,3)
202 for sim in simplices:
203 combinations = [
204 (sim[0], sim[1]), (sim[0], sim[2]), (sim[0], sim[3]),
205 (sim[1], sim[2]), (sim[1], sim[3]), (sim[2], sim[3])
206 ]
207
208 for start, end in combinations:
209 s_idx = int(start)
210 e_idx = int(end)
211 if s_idx < num_input_pts and e_idx < num_input_pts:
212 edge = tuple(sorted((s_idx, e_idx)))
213 edges.add(edge)
214
215 ## Convert Edge to Rhino Line
216 for start_idx, end_idx in edges:
217 line = rg.Line(total_pts[start_idx], total_pts[end_idx])
218 d_lines.append(line)
219
220 return result_cells, d_lines
221
222############################################################
223### EXECUTE ###
224############################################################
225
226M, D = main()