import json import math import numpy as np from compas_tna.diagrams import FormDiagram from compas_tna.envelope import ParametricEnvelope from compas_tno.analysis import Analysis from code.vault_shared import crossvault_middle_hc, fanvault_middle_hc, CONFIG # ---------------------------------------- # 0. Shims and Helpers # ---------------------------------------- class GeneralVaultEnvelope(ParametricEnvelope): def __init__(self, x_span, y_span, thickness, hc, vault_type='cross', discretisation=20, **kwargs): super(GeneralVaultEnvelope, self).__init__(thickness=thickness, is_parametric=True, **kwargs) self.x_span = x_span self.y_span = y_span self.hc = hc self.vault_type = vault_type self.discretisation = discretisation self.middle = None def update_envelope(self): from compas_tna.diagrams.diagram_rectangular import create_cross_mesh n = self.discretisation if isinstance(self.discretisation, int) else self.discretisation[0] self.middle = create_cross_mesh(x_span=self.x_span, y_span=self.y_span, n=n) for vertex in self.middle.vertices(): x, y = self.middle.vertex_attributes(vertex, names=["x", "y"]) z = self.compute_middle([x], [y])[0] self.middle.vertex_attribute(vertex, "z", z) def compute_middle(self, x, y): if self.vault_type == 'fan': return fanvault_middle_hc(x, y, self.x_span, self.y_span, self.hc) return crossvault_middle_hc(x, y, self.x_span, self.y_span, self.hc) def compute_bounds(self, x, y, thickness=None): t = thickness if thickness is not None else self.thickness z_mid = self.compute_middle(x, y) ub = z_mid + t/2 lb = z_mid - t/2 return ub, lb def mesh_to_data(mesh): return { 'vertices': [mesh.vertex_coordinates(v) for v in mesh.vertices()], 'faces': [mesh.face_vertices(f) for f in mesh.faces()] } def diagram_to_wire_data(diagram): return { 'vertices': [diagram.vertex_coordinates(v) for v in diagram.vertices()], 'edges': [list(edge) for edge in diagram.edges()] } def create_custom_fan(x_span, y_span, nx, ny, n_hoops): from compas.datastructures import Mesh from compas_tna.diagrams import FormDiagram x0, x1 = x_span y0, y1 = y_span xm, ym = (x0 + x1) / 2, (y0 + y1) / 2 mesh = Mesh() vertex_map = {} def get_vertex(mesh, x, y): key = (round(x, 6), round(y, 6)) if key in vertex_map: return vertex_map[key] idx = mesh.add_vertex(x=x, y=y, z=0) vertex_map[key] = idx return idx # Quadrant configurations: (corner, nx, ny) quads = [ ((x0, y0), nx, ny), # Q1: BL ((x1, y0), nx, ny), # Q2: BR ((x1, y1), nx, ny), # Q3: TR ((x0, y1), nx, ny) # Q4: TL ] for c, qnx, qny in quads: xc, yc = c # Boundary points for this fan quadrant qpts = [] # Directions towards center lines dx = xm - xc dy = ym - yc # Edge 1: Along the boundary where x varies (at y=ym) # For Q1: (x0, ym) to (xm, ym) for i in range(qnx + 1): t = i / qnx qpts.append((xc + t * dx, ym)) # Edge 2: Along the boundary where y varies (at x=xm) # From (xm, ym) to (xm, yc) for i in range(1, qny + 1): t = i / qny qpts.append((xm, ym - t * dy)) # Build fan layers (hoops) prev_hoop = [get_vertex(mesh, xc, yc)] * len(qpts) for j in range(1, n_hoops + 1): curr_hoop = [] th = j / n_hoops for px, py in qpts: vx = xc + th * (px - xc) vy = yc + th * (py - yc) curr_hoop.append(get_vertex(mesh, vx, vy)) # Add faces for i in range(len(qpts) - 1): if j == 1: mesh.add_face([prev_hoop[i], curr_hoop[i+1], curr_hoop[i]]) else: mesh.add_face([prev_hoop[i], prev_hoop[i+1], curr_hoop[i+1], curr_hoop[i]]) prev_hoop = curr_hoop return FormDiagram.from_mesh(mesh) # ---------------------------------------- # 1. Main Simulation Function # ---------------------------------------- def run_tna_simulation(config=None): if config is None: config = CONFIG xy_span = config['xy_span'] thickness = config['thickness'] max_rise_at_crown = config['max_rise'] vault_type = config['vault_type'] vault = GeneralVaultEnvelope(xy_span[0], xy_span[1], thickness, max_rise_at_crown, vault_type=vault_type, discretisation=config['discretisation_level']) discr_x = config.get('form_discretisation_x', config.get('form_discretisation', 10)) discr_y = config.get('form_discretisation_y', config.get('form_discretisation', 10)) n_hoops = config.get('form_discretisation', 10) if vault_type == 'fan': # form = FormDiagram.create_fan(x_span=xy_span[0], y_span=xy_span[1], n_fans=discretisation, n_hoops=discretisation) form = create_custom_fan(x_span=xy_span[0], y_span=xy_span[1], nx=discr_x, ny=discr_y, n_hoops=n_hoops) else: # For cross vault, we still use symmetric n for now unless specifically requested to refactor its topology n_cross = max(discr_x, discr_y) form = FormDiagram.create_cross(x_span=xy_span[0], y_span=xy_span[1], n=n_cross) # Support logic if config['support_type'] == 'corners': for vertex in form.vertices(): x, y = form.vertex_attributes(vertex, names=['x', 'y']) is_corner = False for cx in xy_span[0]: for cy in xy_span[1]: if abs(x - cx) < 1e-6 and abs(y - cy) < 1e-6: is_corner = True break if is_corner: form.vertex_attribute(vertex, 'is_support', True) else: for vertex in form.vertices(): if form.is_vertex_on_boundary(vertex): form.vertex_attribute(vertex, 'is_support', True) # Set starting point for vertex in form.vertices(): x, y = form.vertex_attributes(vertex, names=["x", "y"]) z_mid = vault.compute_middle([x], [y])[0] form.vertex_attribute(vertex, "z", z_mid) # Solve Min Thrust print(f"Solving Min Thrust for {vault_type}...") form_min = form.copy() analysis_min = Analysis.create_minthrust_analysis(form_min, vault, printout=False, solver=config['solver']) analysis_min.apply_selfweight() analysis_min.apply_envelope() analysis_min.set_up_optimiser() analysis_min.run() # Solve Max Thrust print(f"Solving Max Thrust for {vault_type}...") form_max = form.copy() analysis_max = Analysis.create_maxthrust_analysis(form_max, vault, printout=False, solver=config['solver']) analysis_max.apply_selfweight() analysis_max.apply_envelope() analysis_max.set_up_optimiser() analysis_max.run() # Generate intrados/extrados vault.update_envelope() intrados = vault.middle.copy() extrados = vault.middle.copy() for v in intrados.vertices(): z = intrados.vertex_attribute(v, 'z') intrados.vertex_attribute(v, 'z', z - thickness/2) extrados.vertex_attribute(v, 'z', z + thickness/2) data = { 'intrados': mesh_to_data(intrados), 'extrados': mesh_to_data(extrados), 'thrust_min': diagram_to_wire_data(form_min), 'thrust_max': diagram_to_wire_data(form_max), 'form_min': form_min, 'form_max': form_max } return data if __name__ == '__main__': f_min, f_max = run_tna_simulation() # Optional: still export if run as script f_min.to_json('thrust_min.json') f_max.to_json('thrust_max.json') print("Simulated and exported thrust diagrams.")